diff --git a/.devcontainer/compose.yml b/.devcontainer/compose.yml index 501f78c814..1b4e8b65f6 100644 --- a/.devcontainer/compose.yml +++ b/.devcontainer/compose.yml @@ -36,7 +36,7 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: misskey volumes: - - postgres-data:/var/lib/postgresql/data + - postgres-data:/var/lib/postgresql healthcheck: test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" interval: 5s diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 514abdfb20..3f888504b7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,7 +19,6 @@ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", "Vue.volar", - "Orta.vscode-jest", "dbaeumer.vscode-eslint", "mrmlnc.vscode-json5" ] diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b93080278d..61e222b6f8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,9 +34,6 @@ updates: patterns: - "storybook*" - "@storybook/*" - swc-core: - patterns: - - "@swc/core*" typescript-eslint: patterns: - "@typescript-eslint/*" diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 7d19678574..4e7ff2c3c4 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -28,6 +28,9 @@ jobs: cache: 'pnpm' # see https://docs.github.com/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages#publishing-packages-to-the-npm-registry registry-url: 'https://registry.npmjs.org' + # Ensure npm 11.5.1 or later is installed + - name: Update npm + run: npm install -g npm@latest - name: Publish package run: | pnpm i --frozen-lockfile diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e46890ac..2826b41a65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,57 @@ -## Unreleased +## 2026.5.1 ### General - Feat: ユーザーミュートの適用範囲から通知を除外できるように - タイムラインや検索等でノートが見えないようにしつつ、通知は引き続き受け取れるように設定することができるようになりました +- Fix: `.devcontainer/compose.yml`のvolumeのマウントパスを修正 ### Client - Enhance: ミュートの付与期間を自由に設定できるように - Enhance: ロールの付与期間を自由に設定できるように +- Fix: ロール設定画面でロールをアサイン/アサイン解除した際、リロードしなくても画面に反映されるよう修正 ### Server -- Fix: `/api-doc` にアクセスできない問題を修正 +- Fix: ID生成アルゴリズムにULIDを使用している場合に通知が約10秒遅延する問題を修正 +- Fix: 公開範囲がフォロワーの投稿が通知されない問題を修正 +- Fix: URLプレビューが動作しない問題を修正 +## 2026.5.0 + +### General +- Enhance: アバターデコレーションにカテゴリを設定できるように + +### Client +- Enhance: チャンネル指定リノートでリノート先のチャンネルに移動できるように +- Enhance: ベータ版でのアップデート時のダイアログの更新情報リンクをGitHubのReleasesページに遷移するようにし、正しく閲覧できるように +- Fix: 一部のページ内リンクが正しく動作しない問題を修正 +- Fix: ドライブへの画像アップロード時にファイル名の変更が無視される不具合を修正 +- Fix: 連合が無効化されたサーバーで一部の設定項目が空欄で表示される問題を修正 +- Fix: オーディオ、動画の再生速度メニューが開けない問題を修正 + +### Server +- Enhance: メモリ使用量を削減 +- Enhance: 起動の高速化 + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1410) +- Enhance: バックエンドの開発モード時の安定性向上 +- Enhance: バックエンドビルド・テスト時に使用する依存関係の整理(swc/esbuild→Rolldown, Jest→Vitest) +- Fix: ファイルシステムを用いる処理におけるパスの取り扱いを改善 +- Fix: `/api-doc` にアクセスできない問題を修正 +- Fix: support `alsoKnownAs` from remote actors as either array or unwrapped singleton +- Fix: ローカルに存在しないリモートアカウントに対するアカウント削除リクエストを受信した際に、そのユーザーを新規作成して削除する挙動を修正 +- Fix: Inboxでの特定のエラーによる失敗はDelayedにしない +- Fix: ID生成アルゴリズムにULIDを使用している場合にMisskeyが正しく動作しない問題を修正 +- Fix: リレー経由で届いたノートがリノートとして表示される問題を修正 +- Fix: robots.txtの内容を調整 +- Fix: 特定のユーザーに管理者権限を持つロールが複数ついている際に、取得できるユーザーIDが重複する問題を修正 + (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/17ed4108cec4b6bd2fd989db5a9091db91fa37a7) +- Fix: ブロックしたサーバーからのInboxジョブが蓄積し続ける問題を修正 + (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/3f0f4bfe923f2b3a7837017b54841598f421c6ef) +- Fix: support activity with `actor` as an id string or embedded object in inbox processor and ActivityPub inbox service +- Fix: コンフィグファイルに `meilisearch` の設定がある状態でほかの検索プロバイダを利用すると、UI上からリモートのノートの検索ができない問題を修正 +- Fix: ノートに関する通知で公開範囲が考慮されていない問題を修正 + (Cherry-picked from https://github.com/lqvp/misskey-tempura/commit/cbce96c520a138b8bcd16890ff6f2952830fa166 originally presented in https://github.com/yojo-art/cherrypick/pull/743) + ## 2026.3.2 ### General @@ -45,7 +85,7 @@ - `users/following` の `birthday` プロパティは非推奨になりました。代わりに `users/get-following-users-by-birthday` をご利用ください。 ### General -- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように +- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように (Cherry-picked from https://github.com/MisskeyIO/misskey) - 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました - Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正 @@ -101,9 +141,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Client - Enhance: デッキのUI説明を追加 - Enhance: 設定がブラウザによって消去されないようにするオプションを追加 -- Fix: バージョン表記のないPlayが正しく動作しない問題を修正 +- Fix: バージョン表記のないPlayが正しく動作しない問題を修正 バージョン表記のないものは v0.x 系として実行されます。v1.x 系で動作させたい場合は必ずバージョン表記を含めてください。 -- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正 +- Fix: デッキUIでメニュー位置を下にしているとプロファイル削除ボタンが表示されないのを修正 - Fix: 一部のUnicode絵文字のリアクションがボタンにならない問題を修正 ### Server @@ -148,11 +188,11 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: ページの内容がはみ出ることがある問題を修正 - Fix: ナビゲーションバーを下に表示しているときに、項目数が多いと表示が崩れる問題を修正 - Fix: ヘッダーメニューのチャンネルの新規作成の項目でチャンネル作成ページに飛べない問題を修正 #16816 -- Fix: ラジオボタンに空白の選択肢が表示される問題を修正 +- Fix: ラジオボタンに空白の選択肢が表示される問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1105) - Fix: 一部のシチュエーションで投稿フォームのツアーが正しく表示されない問題を修正 - Fix: 投稿フォームのリセットボタンで注釈がリセットされない問題を修正 -- Fix: PlayのAiScriptバージョン判定(v0.x系・v1.x系の判定)が正しく動作しない問題を修正 +- Fix: PlayのAiScriptバージョン判定(v0.x系・v1.x系の判定)が正しく動作しない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1129) - Fix: フォロー申請をキャンセルする際の確認ダイアログの文言が不正確な問題を修正 - Fix: 初回読み込み時にエラーになることがある問題を修正 @@ -162,12 +202,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Server - Enhance: メモリ使用量を削減しました - Enhance: 依存関係の更新 -- Fix: ワードミュートの文字数計算を修正 +- Fix: ワードミュートの文字数計算を修正 - Fix: チャンネルのリアルタイム更新時に、ロックダウン設定にて非ログイン時にノートを表示しない設定にしている場合でもノートが表示されてしまう問題を修正 -- Fix: DeepL APIのAPIキー指定方式変更に対応 +- Fix: DeepL APIのAPIキー指定方式変更に対応 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1096) - 内部実装の変更にて対応可能な更新です。Misskey側の設定方法に変更はありません。 -- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正 +- Fix: DBレプリケーションを利用する環境でクエリーが失敗する問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1123) ## 2025.11.0 @@ -210,7 +250,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ## 2025.10.1 ### General -- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン) +- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン) パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。 - 依存関係の更新 @@ -337,7 +377,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: レンダリングパフォーマンスの向上 - Enhance: 依存ソフトウェアの更新 - Fix: 投稿フォームでファイルのアップロードが中止または失敗した際のハンドリングを修正 -- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 +- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) - Fix: テーマエディタが動作しない問題を修正 - Fix: チャンネルのハイライトページにノートが表示されない問題を修正 @@ -497,7 +537,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 - Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように - Enhance: リプライ元にアンケートがあることが表示されるように -- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 +- Enhance: ノートのサーバー情報のデザインを改善・パフォーマンス向上 (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283) - Enhance: ユーザー設定でURLプレビューを無効化できるように - Enhance: ヒントとコツを追加 @@ -586,7 +626,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Server - Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に -- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように +- Enhance: フォローしているユーザーならフォロワー限定投稿のノートでもアンテナで検知できるように (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/568 and https://github.com/team-shahu/misskey/pull/38) - Enhance: ユーザーごとにノートの表示が高速化するように - Fix: システムアカウントの名前がサーバー名と同期されない問題を修正 @@ -692,7 +732,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### General - Enhance: プロキシアカウントをシステムアカウントとして作成するように -- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように +- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように 書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。 - Fix: システムアカウントが削除できる問題を修正 @@ -706,7 +746,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Server - Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正 -- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正 +- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895) @@ -727,7 +767,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: リアクションする際に確認ダイアログを表示できるように - Enhance: コントロールパネルのユーザ検索で入力された情報をページ遷移で損なわないように `#15437` - Enhance: CWの注釈で入力済みの文字数を表示 -- Enhance: ノート検索ページのデザイン調整 +- Enhance: ノート検索ページのデザイン調整 (Cherry-picked from https://github.com/taiyme/misskey/pull/273) - Fix: ノートページで、クリップ一覧が表示されないことがある問題を修正 - Fix: コンディショナルロールを手動で割り当てできる導線を削除 `#13529` @@ -744,7 +784,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように - Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正 - Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 ) -- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正 +- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886) - Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように - Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように @@ -754,12 +794,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ## 2025.2.0 ### General -- Fix: Docker のビルドに失敗する問題を修正 +- Fix: Docker のビルドに失敗する問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/883) ### Client - Fix: パスキーでパスワードレスログインが出来ない問題を修正 -- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題 +- Fix: 一部環境でセンシティブなファイルを含むノートの非表示が効かない問題 - Fix: データセーバー有効時にもユーザーページの「ファイル」タブで画像が読み込まれてしまう問題を修正 - Fix: MFMの `sparkle` エフェクトが正しく表示されない問題を修正 - Fix: ページのURLにスラッシュが含まれている場合にページが正しく表示されない問題を修正 @@ -786,14 +826,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` * β版として公開のため、旧画面も引き続き利用可能です ### Client -- Enhance: PC画面でチャンネルが複数列で表示されるように +- Enhance: PC画面でチャンネルが複数列で表示されるように (Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13) - Enhance: 照会に失敗した場合、その理由を表示するように - Enhance: ワードミュートで検知されたワードを表示できるように - Enhance: リモートのノートのリンクをコピーできるように - Enhance: 連合がホワイトリスト化・無効化されているサーバー向けのデザイン修正 - Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加 -- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加 +- Enhance: ノートの添付ファイルを一覧で遡れる「ファイル」タブを追加 (Based on https://github.com/Otaku-Social/maniakey/pull/14) - Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に - Enhance: クエリパラメータでuiを一時的に変更できるように #15240 @@ -801,26 +841,26 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正 - Fix: サーバー情報メニューに区切り線が不足していたのを修正 - Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正 -- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正 +- Fix: 公開範囲がホームのノートの埋め込みウィジェットが読み込まれない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/803) - Fix: 絵文字管理画面で一部の絵文字が表示されない問題を修正 - Fix: プラグイン `register_note_view_interruptor` でノートのサーバー情報の書き換えができない問題を修正 - Fix: Botプロテクションの設定変更時は実際に検証を通過しないと保存できないように( #15137 ) - Fix: ノート検索が使用できない場合でもチャンネルのノート検索欄がでていた問題を修正 - Fix: `Ui:C:select`で値の変更が画面に反映されない問題を修正 -- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 +- Fix: MiAuth認可画面で、認可処理に失敗した場合でもコールバックURLに遷移してしまう問題を修正 (Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4) - Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正 - Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正 - Fix: MacOSでChrome系ブラウザを使用している場合に、Misskeyを閉じた際に他のタブのオーディオ機能と干渉する問題を修正 - Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正 - Fix: 「削除して編集」でノートの引用を解除出来なかった問題を修正( #14476 ) -- Fix: RSSウィジェットが正しく表示されない問題を修正 +- Fix: RSSウィジェットが正しく表示されない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/857) - Fix: ワードミュートの保存失敗時にAPIエラーが握りつぶされる事があるのを修正 - Fix: アンケートでリモートの絵文字が正しく描画できない問題の修正 (Cherry-picked from https://github.com/yojo-art/cherrypick/pull/153) -- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正 +- Fix: 非ログイン時のサーバー概要画面のメニューボタンが押せないことがあるのを修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/656) - Fix: URLにはじめから`#pswp`が含まれている場合に画像ビューワーがブラウザの戻るボタンで閉じられない問題を修正 - Fix: ロール作成画面で設定できるアイコンデコレーションの最大取付個数を16に制限 @@ -829,18 +869,18 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Server - Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように - Enhance: ノート検索の選択肢としてpgroongaに対応 ( #14730 ) -- Enhance: チャート更新時にDBに同時接続しないように +- Enhance: チャート更新時にDBに同時接続しないように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/830) - Enhance: config(default.yml)からSQLログ全文を出力するか否かを設定可能に ( #15266 ) - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 ) -- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 +- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737) - Fix: ノートの閲覧にログイン必須にしてもFeedでノートが表示されてしまう問題を修正 - Fix: 絵文字の連合でライセンス欄を相互にやり取りするように ( #10859, #14109 ) - Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 ) - Fix: disableClustering設定時の初期化ロジックを調整( #15223 ) - Fix: URLとURIが異なるエンティティの照会に失敗する問題を修正( #15039 ) -- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正 +- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869) - Fix: `/api/pages/update`にて`name`を指定せずにリクエストするとエラーが発生する問題を修正 - Fix: AIセンシティブ判定が arm64 環境で動作しない問題を修正 @@ -866,12 +906,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 ) ### Client -- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように +- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/751) - Enhance: ドライブでソートができるように - Enhance: アイコンデコレーション管理画面の改善 - Enhance: 「単なるラッキー」の取得条件を変更 -- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 ) +- Enhance: 投稿フォームでEscキーを押したときIME入力中ならフォームを閉じないように( #10866 ) - Enhance: MiAuth, OAuthの認可画面の改善 - どのアカウントで認証しようとしているのかがわかるように - 認証するアカウントを切り替えられるように @@ -879,29 +919,29 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: カタルーニャ語 (ca-ES) に対応 - Enhance: 個別お知らせページではMetaタグを出力するように - Enhance: ノート詳細画面にロールのバッジを表示 -- Enhance: 過去に送信したフォローリクエストを確認できるように +- Enhance: 過去に送信したフォローリクエストを確認できるように (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663) - Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 ) - Enhance: リノートメニューに「リノートの詳細」を追加 - Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上 - Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 -- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 +- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768) - Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正 - Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used - Fix: リンク切れを修正 -- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正 +- Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正 (Cherry-picked from https://github.com/taiyme/misskey/pull/305) - Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正 -- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正 +- Fix: 画面幅が狭い環境でデザインが崩れる問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/815) -- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように +- Fix: TypeScriptの型チェック対象ファイルを限定してビルドを高速化するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/725) ### Server - Enhance: DockerのNode.jsを22.11.0に更新 -- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) +- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように + (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) - Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように - Fix: sharedInboxが無いActorに紐づくリモートユーザーを照会できない @@ -909,18 +949,18 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: フォロワーへのメッセージの絵文字をemojisに含めるように - Fix: Nested proxy requestsを検出した際にブロックするように [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) -- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 +- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706) -- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正 +- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711) -- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 +- Fix: ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712) -- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 +- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) -- Fix: User Webhookテスト機能のMock Payloadを修正 -- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) +- Fix: User Webhookテスト機能のMock Payloadを修正 +- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) - Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正 -- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正 +- Fix: Inboxの処理で生じるエラーを誤ってActivityとして処理することがある問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/730) - Fix: セキュリティに関する修正 @@ -947,13 +987,13 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: 個人宛のお知らせは「わかった」を押すと自動的にアーカイブされるように - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正 - Fix: RBT有効時、リノートのリアクションが反映されない問題を修正 -- Fix: キューのエラーログを簡略化するように +- Fix: キューのエラーログを簡略化するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649) ## 2024.10.0 ### Note -- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません) +- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません) - ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。 - なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。 - ユーザーデータを読み込む際の型が一部変更されました。 @@ -973,7 +1013,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Client - Enhance: デザインの調整 - Enhance: ログイン画面の認証フローを改善 -- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正 +- Fix: クライアント上での時間ベースの実績獲得動作が実績獲得後も発動していた問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657) ### Server @@ -991,7 +1031,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Feat: フォローされた際のメッセージを設定できるように - Feat: 連合をホワイトリスト制にできるように - Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445) -- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように +- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680) - Feat: データエクスポートが完了した際に通知を発行するように - Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように @@ -1010,12 +1050,12 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正 -- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正 +- Fix: タッチ画面でレンジスライダーを操作するとツールチップが複数表示される問題を修正 (Cherry-picked from https://github.com/taiyme/misskey/pull/265) -- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 +- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725) - Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 -- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正 +- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110) - Fix: 一部画面のページネーションが動作しにくくなっていたのを修正 ( #12766 , #11449 ) @@ -1024,14 +1064,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 -- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 +- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) - Fix: Continue importing from file if single emoji import fails -- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正 +- Fix: `Retry-After`ヘッダーが送信されなかった問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/8a982c61c01909e7540ff1be9f019df07c3f0624) -- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように +- Fix: サーバーサイドのDOM解析完了時にリソースを開放するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/634) -- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに +- Fix: ``を追って照会するのはOKレスポンスが返却された場合のみに (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633) - Fix: メールにスタイルが適用されていなかった問題を修正 @@ -1060,15 +1100,15 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 - Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正 -- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正 +- Fix: サーバー情報取得の際にモデレーター限定の情報が取得できないことがあるのを修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/582) -- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように +- Fix: 公開範囲がダイレクトのノートをユーザーアクティビティのチャート生成に使用しないように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/679) - Fix: ActivityPubのエンティティタイプ判定で不明なタイプを受け取った場合でも処理を継続するように - キュー処理のつまりが改善される可能性があります - Fix: リバーシの対局設定の変更が反映されないのを修正 - Fix: 無制限にストリーミングのチャンネルに接続できる問題を修正 -- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正 +- Fix: ベースロールのポリシーを変更した際にモデログに記録されないのを修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/700) - Fix: Prevent memory leak from memory caches (#14310) - Fix: More reliable memory cache eviction (#14311) @@ -1100,9 +1140,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 - Enhance: 非ログイン時に他サーバーに遷移するアクションを追加 - Enhance: 非ログイン時のハイライトTLのデザインを改善 -- Enhance: フロントエンドのアクセシビリティ改善 +- Enhance: フロントエンドのアクセシビリティ改善 (Based on https://github.com/taiyme/misskey/pull/226) -- Enhance: サーバー情報ページ・お問い合わせページを改善 +- Enhance: サーバー情報ページ・お問い合わせページを改善 (Cherry-picked from https://github.com/taiyme/misskey/pull/238) - Enhance: AiScriptを0.19.0にアップデート - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) @@ -1111,7 +1151,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように - Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように - Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように -- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように +- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように (Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99) - Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように - Enhance: ブラウザのコンテキストメニューを使用できるように @@ -1119,19 +1159,19 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: リバーシの対局を正しく共有できないことがある問題を修正 -- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 +- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 - Fix: アンテナの編集画面のボタンに隙間を追加 - Fix: テーマプレビューが見れない問題を修正 -- Fix: ショートカットキーが連打できる問題を修正 +- Fix: ショートカットキーが連打できる問題を修正 (Cherry-picked from https://github.com/taiyme/misskey/pull/234) - Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため) -- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正 +- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574) - Fix: Twitchの埋め込みが開けない問題を修正 - Fix: 子メニューの高さがウィンドウからはみ出ることがある問題を修正 - Fix: 個人宛てのダイアログ形式のお知らせが即時表示されない問題を修正 - Fix: 一部の画像がセンシティブ指定されているときに画面に何も表示されないことがあるのを修正 -- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正 +- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672) - Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正 - Fix: deck uiの通知音が重なる問題 (#14029) @@ -1174,14 +1214,14 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` 4. フォローしていない非アクティブなユーザ また、自分自身のアカウントもサジェストされるようになりました。 -- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 +- Fix: 一般ユーザーから見たユーザーのバッジの一覧に公開されていないものが含まれることがある問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/652) - Fix: ユーザーのリアクション一覧でミュート/ブロックが機能していなかった問題を修正 - Fix: FTT有効時にリモートユーザーのノートがHTLにキャッシュされる問題を修正 - Fix: 一部の通知がローカル上のリモートユーザーに対して行われていた問題を修正 - Fix: エラーメッセージの誤字を修正 (#14213) - Fix: ソーシャルタイムラインにローカルタイムラインに表示される自分へのリプライが表示されない問題を修正 -- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 +- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) - Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251 - Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正 @@ -1208,7 +1248,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### General - Feat: エラートラッキングにSentryを使用できるようになりました - Enhance: URLプレビューの有効化・無効化を設定できるように #13569 -- Enhance: アンテナでBotによるノートを除外できるように +- Enhance: アンテナでBotによるノートを除外できるように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) - Enhance: クリップのノート数を表示するように - Enhance: コンディショナルロールの条件として以下を新たに追加 (#13667) @@ -1227,7 +1267,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Client - Feat: アップロードするファイルの名前をランダム文字列にできるように -- Feat: 個別のお知らせにリンクで飛べるように +- Feat: 個別のお知らせにリンクで飛べるように (Based on https://github.com/MisskeyIO/misskey/pull/639) - Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように - Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように @@ -1257,9 +1297,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Fix: 一部のページ内リンクが正しく動作しない問題を修正 - Fix: 周年の実績が閏年を考慮しない問題を修正 - Fix: ローカルURLのプレビューポップアップが左上に表示される -- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正 +- Fix: WebGL2をサポートしないブラウザで「季節に応じた画面の演出」が有効になっているとき、Misskeyが起動できなくなる問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459) -- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正 +- Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528) - Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177 - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。 @@ -1282,13 +1322,13 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: ドライブのファイルがNSFWかどうか個別に連合されるように (#13756) - 可能な場合、ノートの添付ファイルのセンシティブ判定がファイル単位になります - Fix: リモートから配送されたアクティビティにJSON-LD compactionをかける -- Fix: フォローリクエストを作成する際に既存のものは削除するように +- Fix: フォローリクエストを作成する際に既存のものは削除するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) - Fix: エンドポイント`notes/translate`のエラーを改善 - Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) - Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 - Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正 -- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように +- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) - Fix: Add Cache-Control to Bull Board - Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正 @@ -1481,10 +1521,10 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### Note - 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました - 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします -- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 +- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 - **影響:** - それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。 + **影響:** + それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。 投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。 1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。 @@ -1531,7 +1571,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` - Enhance: Unicode 15.0のサポート - Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように - MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました - - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります + - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります (例: ` ```js ` → Javascript, ` ```ais ` → AiScript) - Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように - Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる @@ -1938,9 +1978,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false` ### General - 招待機能を改善しました - * 過去に発行した招待コードを確認できるようになりました - * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました - * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました + * 過去に発行した招待コードを確認できるようになりました + * ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました + * 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました - ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました - identicon生成を無効にしてパフォーマンスを向上させることができるようになりました - サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました @@ -2103,9 +2143,9 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー * 「フォロワーのみ」の投稿は検索結果に表示されません。 - 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加 - ユーザーへの自分用メモ機能 - * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 + * ユーザーに対して、自分だけが見られるメモを追加できるようになりました。 (自分自身に対してもメモを追加できます。) - * ユーザーメニューから追加できます。 + * ユーザーメニューから追加できます。 (デスクトップ表示ではusernameの右側のボタンからも追加可能) - チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。 - チャンネルをアーカイブできるようになりました。 diff --git a/Dockerfile b/Dockerfile index 19f9e8c9dc..d6c8d7e415 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,7 +102,6 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built -COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis COPY --chown=misskey:misskey . ./ diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 6fe7f32cc4..b91e34dc12 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "lib": ["dom", "es5"], - "target": "es5", + "lib": ["dom"], + "target": "esnext", "types": ["cypress", "node"] }, "include": ["./**/*.ts"] diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index f2867585c2..ae7f4a03f1 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1073,8 +1073,8 @@ thisPostMayBeAnnoying: "Aquesta nota pot ser molesta per algú." thisPostMayBeAnnoyingHome: "Publicar a la línia de temps d'Inici" thisPostMayBeAnnoyingCancel: "Cancel·lar " thisPostMayBeAnnoyingIgnore: "Publicar de totes maneres" -collapseRenotes: "Col·lapsar les renotes que ja has vist" -collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has renotat" +collapseRenotes: "Col·lapsar els impulsos que ja has vist" +collapseRenotesDescription: "Col·lapse les notes a les quals ja has reaccionat o que ja has impulsat." internalServerError: "Error intern del servidor" internalServerErrorDescription: "El servidor ha fallat de manera inexplicable." copyErrorInfo: "Copiar la informació de l'error " @@ -1408,6 +1408,7 @@ frame: "Marc" presets: "Predefinit" zeroPadding: "Sense omplir" nothingToConfigure: "No hi ha res a configurar" +viewRenotedChannel: "Mirar el canal d'impulsos " _imageEditing: _vars: caption: "Títol de l'arxiu" @@ -1687,7 +1688,7 @@ _initialTutorial: description: "Pots limitar qui pot veure les teves notes." public: "La teva nota serà visible per a tots els usuaris." home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure." - followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes." + followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer impulsos." direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes." doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible." doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 72b7892128..3c7852cd05 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -3312,7 +3312,7 @@ _clientPerformanceIssueTip: _clip: tip: "Clip es una función que permite organizar varias notas." _userLists: - tip: "Las listas pueden contener cualquier usuario que especifiques al crearlas, la lista creada puede mostrarse entonces como una línea de tiempo mostrando solo los usuarios especificados." + tip: "Puedes crear listas que incluyan a cualquier usuario. Las listas creadas se pueden visualizar en forma de cronología." watermark: "Marca de Agua" defaultPreset: "Por defecto" _watermarkEditor: diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 2401bd84aa..08c9197251 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1,7 +1,7 @@ --- _lang_: "Italiano" headlineMisskey: "Rete collegata tramite Note" -introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n\n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n\n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n\n🚀 Esplora un nuovo mondo insieme a noi!" +introMisskey: "Eccoci! Misskey è un servizio di microblogging decentralizzato, libero e aperto. \n📡 Puoi pubblicare «Note» per condividere ciò che sta succedendo o per dire a tutti qualcosa su di te. \n👍 Puoi reagire inviando emoji rapidi alle «Note» provenienti da altri profili nel Fediverso.\n🚀 Esplora un nuovo mondo insieme a noi!" poweredByMisskeyDescription: "{name} è uno dei servizi (chiamati istanze) che utilizzano la piattaforma open source Misskey." monthAndDay: "{day}/{month}" search: "Cerca" @@ -1408,6 +1408,7 @@ frame: "Cornice" presets: "Preimpostato" zeroPadding: "Al vivo" nothingToConfigure: "Niente da configurare" +viewRenotedChannel: "Visualizza il canale del Rinota" _imageEditing: _vars: caption: "Didascalia dell'immagine" @@ -3338,7 +3339,7 @@ _watermarkEditor: stripeWidth: "Larghezza della linea" stripeFrequency: "Il numero di linee" polkadot: "A pallini" - checker: "revisore" + checker: "Scacchiera" polkadotMainDotOpacity: "Opacità del punto principale" polkadotMainDotRadius: "Dimensione del punto principale" polkadotSubDotOpacity: "Opacità del punto secondario" @@ -3367,7 +3368,7 @@ _imageEffector: zoomLines: "Linea di saturazione" stripe: "Strisce" polkadot: "A pallini" - checker: "revisore" + checker: "Scacchiera" blockNoise: "Attenua rumore" tearing: "Strappa immagine" fill: "Riempimento" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c673dbb621..39c3460cb2 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1413,6 +1413,7 @@ presets: "プリセット" zeroPadding: "ゼロ埋め" muteConfirm: "ミュートしますか?" nothingToConfigure: "設定項目はありません" +viewRenotedChannel: "リノート先のチャンネルを見る" _imageEditing: _vars: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 52da6d071a..294791cce3 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1408,6 +1408,7 @@ frame: "프레임" presets: "프리셋" zeroPadding: "0으로 채우기" nothingToConfigure: "설정 항목이 없습니다." +viewRenotedChannel: "리노트된 채널 보기" _imageEditing: _vars: caption: "파일 설명" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml index 7017d81733..3779155ae1 100644 --- a/locales/lo-LA.yml +++ b/locales/lo-LA.yml @@ -5,6 +5,7 @@ introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນຊອ poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. Misskey (ເອີ້ນວ່າ \"Misskey instance\")" monthAndDay: "ເດືອນ{month} / ວັນ{day}" search: "ຄົ້ນຫາ" +reset: "ຣີເຊັດ" notifications: "ການແຈ້ງເຕືອນ" username: "ຊື່ຜູ້ໃຊ້" password: "ລະຫັດຜ່ານ" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 6bcff59979..bd53d28300 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -3401,6 +3401,8 @@ _imageEffector: threshold: "เทรชโฮลด์" centerX: "กลาง X" centerY: "กลาง Y" + density: "ความหนาทึบ" + zoomLinesOutlineThickness: "ความหนาของเงาเส้น" zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง" circle: "ทรงกลม" drafts: "ร่าง" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 5cfa90e910..cda2fc6531 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1408,6 +1408,7 @@ frame: "边框" presets: "预设值" zeroPadding: "填充 0" nothingToConfigure: "没有项目" +viewRenotedChannel: "查看转帖所属频道" _imageEditing: _vars: caption: "文件标题" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index fa8a3eead8..c1347f54c0 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -10,7 +10,7 @@ notifications: "通知" username: "使用者名稱" password: "密碼" initialPasswordForSetup: "啟動初始設定的密碼" -initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。" +initialPasswordIsIncorrect: "啟動初始設定密碼錯誤。" initialPasswordForSetupDescription: "如果您自己安裝了 Misskey,請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼,請將其留空並繼續。" forgotPassword: "忘記密碼" fetchingAsApObject: "從聯邦宇宙取得中..." @@ -1408,6 +1408,7 @@ frame: "邊框" presets: "預設值" zeroPadding: "補零" nothingToConfigure: "無可設定的項目" +viewRenotedChannel: "顯示轉發貼文者的頻道" _imageEditing: _vars: caption: "檔案標題" diff --git a/package.json b/package.json index b70960417a..60fe45a9c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2026.3.2", + "version": "2026.5.1-alpha.0", "codename": "nasubi", "repository": { "type": "git", @@ -28,9 +28,9 @@ "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js", - "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js", - "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js", + "start": "cd packages/backend && pnpm compile-config && node ./built/entry.js", + "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/entry.js", + "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js", "cli": "cd packages/backend && pnpm cli", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", @@ -44,8 +44,8 @@ "cy:run": "pnpm cypress run", "e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run", "e2e-dev-container": "ncp ./.config/cypress-devcontainer.yml ./.config/test.yml && pnpm start-server-and-test start:test http://localhost:61812 cy:run", - "jest": "cd packages/backend && pnpm jest", - "jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage", + "backend-unit-test": "cd packages/backend && pnpm test", + "backend-unit-test-and-coverage": "cd packages/backend && pnpm test-and-coverage", "test": "pnpm -r test", "test-and-coverage": "pnpm -r test-and-coverage", "clean": "node scripts/clean.mjs", @@ -53,12 +53,12 @@ "cleanall": "pnpm clean-all" }, "dependencies": { - "cssnano": "7.1.3", - "esbuild": "0.27.4", + "cssnano": "7.1.5", + "esbuild": "0.28.0", "execa": "9.6.1", "ignore-walk": "8.0.0", "js-yaml": "4.1.1", - "postcss": "8.5.8", + "postcss": "8.5.9", "tar": "7.5.13", "terser": "5.46.1" }, @@ -66,17 +66,17 @@ "@eslint/js": "9.39.4", "@misskey-dev/eslint-plugin": "2.1.0", "@types/js-yaml": "4.0.9", - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "@typescript/native-preview": "7.0.0-dev.20260116.1", + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@typescript/native-preview": "7.0.0-dev.20260421.2", "cross-env": "10.1.0", - "cypress": "15.13.0", + "cypress": "15.13.1", "eslint": "9.39.4", - "globals": "17.4.0", + "globals": "17.5.0", "ncp": "2.0.0", "pnpm": "10.33.0", - "start-server-and-test": "2.1.5", + "start-server-and-test": "3.0.2", "typescript": "5.9.3" }, "optionalDependencies": { @@ -86,7 +86,7 @@ "overrides": { "@aiscript-dev/aiscript-languageserver": "-", "chokidar": "5.0.0", - "lodash": "4.17.23" + "lodash": "4.18.1" }, "ignoredBuiltDependencies": [ "@sentry-internal/node-cpu-profiler", diff --git a/packages/backend/.swcrc b/packages/backend/.swcrc deleted file mode 100644 index 7e1767a67a..0000000000 --- a/packages/backend/.swcrc +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "https://swc.rs/schema.json", - "jsc": { - "parser": { - "syntax": "typescript", - "jsx": true, - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true, - "react": { - "runtime": "automatic", - "importSource": "@kitajs/html" - } - }, - "experimental": { - "keepImportAssertions": true - }, - "baseUrl": "src", - "paths": { - "@/*": ["*"] - }, - "target": "es2022" - }, - "minify": false, - "sourceMaps": "inline" -} diff --git a/packages/backend/assets/robots.txt b/packages/backend/assets/robots.txt deleted file mode 100644 index dc17e04e3f..0000000000 --- a/packages/backend/assets/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -user-agent: * -allow: / - -# todo: sitemap diff --git a/packages/backend/build.js b/packages/backend/build.js deleted file mode 100644 index 52ca09b7a8..0000000000 --- a/packages/backend/build.js +++ /dev/null @@ -1,121 +0,0 @@ -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, join } from 'node:path'; -import { build } from 'esbuild'; -import { swcPlugin } from 'esbuild-plugin-swc'; - -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); -const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); - -const resolveTsPathsPlugin = { - name: 'resolve-ts-paths', - setup(build) { - build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => { - if (args.importer) { - const absPath = join(args.resolveDir, args.path); - const tsPath = absPath.slice(0, -3) + '.ts'; - if (fs.existsSync(tsPath)) return { path: tsPath }; - const tsxPath = absPath.slice(0, -3) + '.tsx'; - if (fs.existsSync(tsxPath)) return { path: tsxPath }; - } - }); - }, -}; - -const externalIpaddrPlugin = { - name: 'external-ipaddr', - setup(build) { - build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => { - return { path: args.path, external: true }; - }); - }, -}; - -/** @type {import('esbuild').BuildOptions} */ -const options = { - entryPoints: ['./src/boot/entry.ts'], - minify: true, - keepNames: true, - bundle: true, - outdir: './built/boot', - target: 'node22', - platform: 'node', - format: 'esm', - sourcemap: 'linked', - packages: 'external', - banner: { - js: 'import { createRequire as topLevelCreateRequire } from "module";' + - 'import ___url___ from "url";' + - 'const require = topLevelCreateRequire(import.meta.url);' + - 'const __filename = ___url___.fileURLToPath(import.meta.url);' + - 'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));', - }, - plugins: [ - externalIpaddrPlugin, - resolveTsPathsPlugin, - swcPlugin({ - jsc: { - parser: { - syntax: 'typescript', - decorators: true, - dynamicImport: true, - }, - transform: { - legacyDecorator: true, - decoratorMetadata: true, - }, - experimental: { - keepImportAssertions: true, - }, - baseUrl: join(_dirname, 'src'), - paths: { - '@/*': ['*'], - }, - target: 'esnext', - keepClassNames: true, - }, - }), - externalIpaddrPlugin, - ], - // external: [ - // 'slacc-*', - // 'class-transformer', - // 'class-validator', - // '@sentry/*', - // '@nestjs/websockets/socket-module', - // '@nestjs/microservices/microservices-module', - // '@nestjs/microservices', - // '@napi-rs/canvas-win32-x64-msvc', - // 'mock-aws-s3', - // 'aws-sdk', - // 'nock', - // 'sharp', - // 'jsdom', - // 're2', - // '@napi-rs/canvas', - // ], -}; - -const args = process.argv.slice(2).map(arg => arg.toLowerCase()); - -if (!args.includes('--no-clean')) { - fs.rmSync('./built', { recursive: true, force: true }); -} - -await buildSrc(); - -async function buildSrc() { - console.log(`[${_package.name}] start building...`); - - await build(options) - .then(() => { - console.log(`[${_package.name}] build succeeded.`); - }) - .catch((err) => { - process.stderr.write(err.stderr || err.message || err); - process.exit(1); - }); - - console.log(`[${_package.name}] finish building.`); -} diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs deleted file mode 100644 index 22ffbbee5c..0000000000 --- a/packages/backend/jest.config.cjs +++ /dev/null @@ -1,220 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -module.exports = { - // All imported modules in your tests should be mocked automatically - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest", - - // Automatically clear mock calls and instances between every test - // clearMocks: false, - - // Indicates whether the coverage information should be collected while executing the test - // collectCoverage: false, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], - - // The directory where Jest should output its coverage files - coverageDirectory: "coverage", - - // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", - - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], - - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, - - // A path to a custom dependency extractor - // dependencyExtractor: undefined, - - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, - - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], - - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, - - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, - - // A set of global variables that need to be available in all test environments - globals: { - }, - - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", - - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], - - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "json", - // "jsx", - // "ts", - // "tsx", - // "node" - // ], - - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - moduleNameMapper: { - // Do not resolve .wasm.js to .wasm by the rule below - '^(.+)\\.wasm\\.js$': '$1.wasm.js', - // SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule - // converts it again to `../../src/foo/bar` which then can be resolved to - // `.ts` files. - // See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225 - // TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can - // directly import `.ts` files without this hack. - '^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1', - }, - - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], - - // Activates notifications for test results - // notify: false, - - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", - - // A preset that is used as a base for Jest's configuration - //preset: "ts-jest/presets/js-with-ts-esm", - - // Run tests from one or more projects - // projects: undefined, - - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, - - // Automatically reset mock state between every test - // resetMocks: false, - - // Reset the module registry before running each individual test - // resetModules: false, - - // A path to a custom resolver - // resolver: './jest-resolver.cjs', - - // Automatically restore mock state between every test - restoreMocks: true, - - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, - - // A list of paths to directories that Jest should use to search for files in - roots: [ - "" - ], - - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", - - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], - - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], - - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, - - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], - - // The test environment that will be used for testing - testEnvironment: "node", - - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - - // Adds a location field to test results - // testLocationInResults: false, - - // The glob patterns Jest uses to detect test files - testMatch: [ - "/test/unit/**/*.ts", - "/src/**/*.test.ts", - ], - - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], - - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], - - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, - - // This option allows use of a custom test runner - // testRunner: "jasmine2", - - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", - - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", - - // A map from regular expressions to paths to transformers - transform: { - "^.+\\.(t|j)sx?$": ["@swc/jest"], - }, - - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "\\\\node_modules\\\\", - // "\\.pnp\\.[^\\\\]+$" - // ], - - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, - - // Indicates whether each individual test should be reported during the run - // verbose: undefined, - - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], - - // Whether to use watchman for file crawling - // watchman: true, - - extensionsToTreatAsEsm: ['.ts', '.tsx'], - - testTimeout: 60000, - - // Let Jest kill the test worker whenever it grows too much - // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest) - // https://github.com/facebook/jest/issues/11956 - maxWorkers: 1, // Make it use worker (that can be killed and restarted) - logHeapUsage: true, // To debug when out-of-memory happens on CI - workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - - maxConcurrency: 32, -}; diff --git a/packages/backend/jest.config.e2e.cjs b/packages/backend/jest.config.e2e.cjs deleted file mode 100644 index 4502da47df..0000000000 --- a/packages/backend/jest.config.e2e.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - globalSetup: "/built-test/entry.js", - setupFilesAfterEnv: ["/test/jest.setup.ts"], - testMatch: [ - "/test/e2e/**/*.ts", - ], -}; diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs deleted file mode 100644 index fae187bc23..0000000000 --- a/packages/backend/jest.config.fed.cjs +++ /dev/null @@ -1,13 +0,0 @@ -/* - * For a detailed explanation regarding each configuration property and type check, visit: - * https://jestjs.io/docs/en/configuration.html - */ - -const base = require('./jest.config.cjs'); - -module.exports = { - ...base, - testMatch: [ - '/test-federation/test/**/*.test.ts', - ], -}; diff --git a/packages/backend/jest.config.unit.cjs b/packages/backend/jest.config.unit.cjs deleted file mode 100644 index 957d0635c1..0000000000 --- a/packages/backend/jest.config.unit.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/* -* For a detailed explanation regarding each configuration property and type check, visit: -* https://jestjs.io/docs/en/configuration.html -*/ - -const base = require('./jest.config.cjs') - -module.exports = { - ...base, - globalSetup: "/test/jest.setup.unit.cjs", - testMatch: [ - "/test/unit/**/*.ts", - "/src/**/*.test.ts", - ], -}; diff --git a/packages/backend/jest.js b/packages/backend/jest.js deleted file mode 100644 index 61f6b00e85..0000000000 --- a/packages/backend/jest.js +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env node -import child_process from 'node:child_process'; -import path from 'node:path'; -import url from 'node:url'; - -import semver from 'semver'; - -const __filename = url.fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const args = []; -args.push(...[ - ...semver.satisfies(process.version, '^20.17.0 || ^22.0.0 || ^24.10.0') ? ['--no-experimental-require-module'] : [], - '--experimental-vm-modules', - '--experimental-import-meta-resolve', - path.join(__dirname, 'node_modules/jest/bin/jest.js'), - ...process.argv.slice(2), -]); - -const child = child_process.spawn(process.execPath, args, { stdio: 'inherit' }); -child.on('error', (err) => { - console.error('Failed to start Jest:', err); - process.exit(1); -}); -child.on('exit', (code, signal) => { - if (code === null) { - process.exit(128 + signal); - } else { - process.exit(code); - } -}); diff --git a/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js new file mode 100644 index 0000000000..a3410aa88e --- /dev/null +++ b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddCategoryToAvatarDecorations1766652173085 { + name = 'AddCategoryToAvatarDecorations1766652173085'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" ADD "category" character varying(128)'); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" DROP COLUMN "category"'); + } +}; diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index 1a8c146451..dabc0893f4 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,6 +1,6 @@ import { DataSource } from 'typeorm'; -import { loadConfig } from './src-js/config.js'; -import { entities } from './src-js/postgres.js'; +import { loadConfig } from './built/config.js'; +import { entities } from './built/postgres.js'; const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 40d963f3c7..e16f335007 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -7,51 +7,33 @@ "node": "^22.15.0 || ^24.10.0" }, "scripts": { - "start": "pnpm compile-config && node ./built/boot/entry.js", - "start:inspect": "pnpm compile-config && node --inspect ./built/boot/entry.js", - "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js", + "start": "pnpm compile-config && node ./built/entry.js", + "start:inspect": "pnpm compile-config && node --inspect ./built/entry.js", + "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js", "migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js", - "cli": "pnpm compile-config && node ./src-js/boot/cli.js", + "cli": "pnpm compile-config && node ./built/cli.js", "check:connect": "pnpm compile-config && node ./scripts/check_connect.js", "compile-config": "node ./scripts/compile_config.js", - "build": "swc src -d src-js -D --strip-leading-paths && node ./build.js", - "build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths", - "watch:swc": "swc src -d built -D -w --strip-leading-paths", + "build": "rolldown -c", + "build:unit": "rolldown -c --sourcemap", + "build:e2e": "rolldown -c --e2e", "build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json", "watch": "pnpm compile-config && node ./scripts/watch.mjs", "restart": "pnpm build && pnpm start", - "dev": "pnpm compile-config && node ./scripts/dev.mjs", + "dev": "pnpm compile-config && rolldown -c --watch", "typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit", "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", - "jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs", - "jest:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs", - "jest:fed": "pnpm compile-config && node ./jest.js --forceExit --config jest.config.fed.cjs", - "jest-and-coverage": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs", - "jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs", - "jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache", - "test": "pnpm jest", - "test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e", - "test:fed": "pnpm jest:fed", - "test-and-coverage": "pnpm jest-and-coverage", - "test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e", + "test": "pnpm build:unit && cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.unit.ts", + "test:e2e": "pnpm build:e2e && cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.e2e.ts", + "test:fed": "cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.fed.ts", + "test-and-coverage": "pnpm build:unit && cross-env NODE_ENV=test pnpm compile-config && vitest --coverage --config vitest.config.unit.ts", + "test-and-coverage:e2e": "pnpm build:e2e && cross-env NODE_ENV=test pnpm compile-config && vitest --coverage --config vitest.config.e2e.ts", "check-migrations": "node scripts/check_migrations_clean.js", "generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js" }, "optionalDependencies": { - "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.15.21", - "@swc/core-darwin-x64": "1.15.21", - "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.15.21", - "@swc/core-linux-arm64-gnu": "1.15.21", - "@swc/core-linux-arm64-musl": "1.15.21", - "@swc/core-linux-x64-gnu": "1.15.21", - "@swc/core-linux-x64-musl": "1.15.21", - "@swc/core-win32-arm64-msvc": "1.15.21", - "@swc/core-win32-ia32-msvc": "1.15.21", - "@swc/core-win32-x64-msvc": "1.15.21", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.1.0", @@ -71,30 +53,29 @@ "utf-8-validate": "6.0.6" }, "dependencies": { - "@aws-sdk/client-s3": "3.1016.0", - "@aws-sdk/lib-storage": "3.1016.0", + "@aws-sdk/client-s3": "3.1030.0", + "@aws-sdk/lib-storage": "3.1030.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.4", "@fastify/cors": "11.2.0", - "@fastify/express": "4.0.4", - "@fastify/http-proxy": "11.4.2", - "@fastify/multipart": "9.4.0", - "@fastify/static": "9.0.0", + "@fastify/express": "4.0.5", + "@fastify/http-proxy": "11.4.4", + "@fastify/multipart": "10.0.0", + "@fastify/static": "9.1.3", "@kitajs/html": "4.2.13", "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.2.5", + "@misskey-dev/summaly": "5.3.0", "@napi-rs/canvas": "0.1.97", - "@nestjs/common": "11.1.17", - "@nestjs/core": "11.1.17", - "@nestjs/testing": "11.1.17", + "@nestjs/common": "11.1.19", + "@nestjs/core": "11.1.19", + "@nestjs/testing": "11.1.19", + "@oxc-project/runtime": "0.125.0", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.45.0", - "@sentry/profiling-node": "10.45.0", + "@sentry/node": "10.48.0", + "@sentry/profiling-node": "10.48.0", "@simplewebauthn/server": "13.3.0", - "@sinonjs/fake-timers": "15.1.1", - "@smithy/node-http-handler": "4.5.0", - "@swc/cli": "0.8.0", - "@swc/core": "1.15.21", + "@sinonjs/fake-timers": "15.3.2", + "@smithy/node-http-handler": "4.5.2", "@twemoji/parser": "16.0.0", "accepts": "1.3.8", "ajv": "8.18.0", @@ -103,19 +84,19 @@ "bcryptjs": "3.0.3", "blurhash": "2.0.5", "body-parser": "2.2.2", - "bullmq": "5.71.0", + "bullmq": "5.73.5", "cacheable-lookup": "7.0.0", "chalk": "5.6.2", "chalk-template": "1.1.2", "chokidar": "5.0.0", "color-convert": "3.1.3", - "content-disposition": "1.0.1", + "content-disposition": "1.1.0", "date-fns": "4.1.0", "deep-email-validator": "0.1.21", - "fastify": "5.8.4", + "fastify": "5.8.5", "fastify-raw-body": "5.0.0", "feed": "5.2.0", - "file-type": "21.3.4", + "file-type": "22.0.1", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.5", "got": "14.6.6", @@ -129,7 +110,7 @@ "json5": "2.2.3", "jsonld": "9.0.0", "juice": "11.1.1", - "meilisearch": "0.56.0", + "meilisearch": "0.57.0", "mfm-js": "0.25.0", "mime-types": "3.0.2", "misskey-js": "workspace:*", @@ -139,7 +120,7 @@ "nested-property": "4.0.0", "node-fetch": "3.3.2", "node-html-parser": "7.1.0", - "nodemailer": "8.0.3", + "nodemailer": "8.0.5", "nsfwjs": "4.3.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", @@ -152,12 +133,12 @@ "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.23.3", + "re2": "1.24.0", "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.2", - "sanitize-html": "2.17.2", + "sanitize-html": "2.17.3", "secure-json-parse": "4.1.0", "semver": "7.7.4", "sharp": "0.33.5", @@ -176,25 +157,24 @@ "xev": "3.0.2" }, "devDependencies": { - "@jest/globals": "29.7.0", "@kitajs/ts-html-plugin": "4.1.4", - "@nestjs/platform-express": "11.1.17", - "@sentry/vue": "10.45.0", + "@nestjs/platform-express": "11.1.19", + "@rollup/plugin-esm-shim": "0.1.8", + "@sentry/vue": "10.48.0", "@simplewebauthn/types": "12.0.0", - "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", "@types/archiver": "7.0.0", "@types/body-parser": "1.19.6", - "@types/color-convert": "2.0.4", + "@types/color-convert": "3.0.1", "@types/content-disposition": "0.5.9", "@types/fluent-ffmpeg": "2.1.28", "@types/http-link-header": "1.0.7", - "@types/jest": "29.5.14", + "@types/js-yaml": "4.0.9", "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", "@types/ms": "2.1.0", - "@types/node": "24.12.0", - "@types/nodemailer": "7.0.11", + "@types/node": "24.12.2", + "@types/nodemailer": "8.0.0", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.20.0", @@ -206,28 +186,28 @@ "@types/semver": "7.7.1", "@types/simple-oauth2": "5.0.8", "@types/sinonjs__fake-timers": "15.0.1", - "@types/supertest": "6.0.3", + "@types/supertest": "7.2.0", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@vitest/coverage-v8": "4.1.4", "aws-sdk-client-mock": "4.1.0", "cbor": "10.0.12", "cross-env": "10.1.0", - "esbuild-plugin-swc": "1.0.1", "eslint-plugin-import": "2.32.0", "execa": "9.6.1", "fkill": "10.0.3", - "jest": "29.7.0", - "jest-mock": "29.7.0", "js-yaml": "4.1.1", - "nodemon": "3.1.14", - "pid-port": "2.1.0", + "pid-port": "2.1.1", + "rolldown": "1.0.0-rc.15", "simple-oauth2": "5.1.0", "supertest": "7.2.2", - "vite": "8.0.2" + "vite": "8.0.8", + "vitest": "4.1.4", + "vitest-mock-extended": "4.0.0" } } diff --git a/packages/backend/rolldown.config.ts b/packages/backend/rolldown.config.ts new file mode 100644 index 0000000000..950bc63560 --- /dev/null +++ b/packages/backend/rolldown.config.ts @@ -0,0 +1,128 @@ +import { defineConfig } from 'rolldown'; +import type { Plugin, ExternalOption } from 'rolldown'; +import { execa, execaNode } from 'execa'; +import type { ResultPromise } from 'execa'; +import esmShim from '@rollup/plugin-esm-shim'; + +/** + * Watchモード時にバックエンドの起動・停止制御を行うプラグイン + */ +function backendDevServerPlugin(): Plugin { + let backendProcess: ResultPromise | null = null; + + async function runBuildAssets() { + await execa('pnpm', ['run', 'build-assets'], { + cwd: '../../', + stdout: process.stdout, + stderr: process.stderr, + }); + } + + async function killBackendProcess() { + if (backendProcess) { + backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す + backendProcess.kill(); + await new Promise(resolve => backendProcess!.on('exit', resolve)); + backendProcess = null; + } + } + + return { + name: 'backend-dev-server', + async closeBundle() { + await runBuildAssets(); + if (backendProcess) { + await killBackendProcess(); + } + backendProcess = execaNode('./built/entry.js', [], { + stdout: process.stdout, + stderr: process.stderr, + env: { + NODE_ENV: 'development', + }, + }); + }, + async watchChange() { + if (backendProcess) { + await killBackendProcess(); + await runBuildAssets(); + } + }, + }; +} + +export default defineConfig((args) => { + const isWatchMode = args.watch != null && args.watch !== 'false'; + const isE2E = args.e2e != null && args.e2e !== 'false'; + + // 通常のビルド時にexternalとするモジュール + const externalModules: ExternalOption = [ + /^slacc-.*/, + 'class-transformer', + 'class-validator', + /^@sentry\/.*/, + /^@sentry-internal\/.*/, + '@nestjs/websockets/socket-module', + '@nestjs/microservices/microservices-module', + '@nestjs/microservices', + /^@napi-rs\/.*/, + 'mock-aws-s3', + 'aws-sdk', + 'nock', + 'sharp', + 'jsdom', + 're2', + 'ipaddr.js', + 'oauth2orize', + 'file-type', + ]; + + if (isE2E) { + return { + input: './test-server/entry.ts', + platform: 'node', + tsconfig: './test-server/tsconfig.json', + plugins: [ + esmShim(), + ], + output: { + keepNames: true, + sourcemap: true, + dir: './built-test', + cleanDir: true, + format: 'esm', + }, + external: externalModules, + }; + } else { + return { + input: [ + './src/boot/entry.ts', + './src/boot/cli.ts', + './src/config.ts', + './src/postgres.ts', + './src/server/api/openapi/gen-spec.ts', + ], + platform: 'node', + tsconfig: true, + plugins: [ + esmShim(), + (isWatchMode ? backendDevServerPlugin() : undefined), + ], + output: { + keepNames: true, + minify: !isWatchMode, + sourcemap: isWatchMode, + dir: './built', + cleanDir: !isWatchMode, + format: 'esm', + }, + watch: { + include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'], + clearScreen: false, + }, + // ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする + external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules, + }; + } +}); diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index a1cb839303..9e2f214e93 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -4,8 +4,8 @@ */ import Redis from 'ioredis'; -import { loadConfig } from '../src-js/config.js'; -import { createPostgresDataSource } from '../src-js/postgres.js'; +import { loadConfig } from '../built/config.js'; +import { createPostgresDataSource } from '../built/postgres.js'; const config = loadConfig(); diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs deleted file mode 100644 index db96eaf976..0000000000 --- a/packages/backend/scripts/dev.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa, execaNode } from 'execa'; - -/** @type {import('execa').ExecaChildProcess | undefined} */ -let backendProcess; - -async function execBuildAssets() { - await execa('pnpm', ['run', 'build-assets'], { - cwd: '../../', - stdout: process.stdout, - stderr: process.stderr, - }) -} - -function execStart() { - // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので - // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい - backendProcess = execaNode('./built/boot/entry.js', [], { - stdout: process.stdout, - stderr: process.stderr, - env: { - 'NODE_ENV': 'development', - }, - }); -} - -async function killProc() { - if (backendProcess) { - backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す - backendProcess.kill(); - await new Promise(resolve => backendProcess.on('exit', resolve)); - backendProcess = undefined; - } -} - -(async () => { - execaNode( - './node_modules/nodemon/bin/nodemon.js', - [ - '-w', 'src', - '-e', 'ts,js,mjs,cjs,tsx,json,pug', - '--exec', 'pnpm', 'run', 'build', - ], - { - stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], - serialization: "json", - }) - .on('message', async (message) => { - if (message.type === 'exit') { - // かならずbuild->build-assetsの順番で呼び出したいので、 - // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。 - // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある - - await killProc(); - await execBuildAssets(); - execStart(); - } - }) -})(); diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js index 237f63a4d3..8a7e0b062d 100644 --- a/packages/backend/scripts/generate_api_json.js +++ b/packages/backend/scripts/generate_api_json.js @@ -19,10 +19,10 @@ async function main() { } /** @type {import('../src/config.js')} */ - const { loadConfig } = await import('../src-js/config.js'); + const { loadConfig } = await import('../built/config.js'); /** @type {import('../src/server/api/openapi/gen-spec.js')} */ - const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js'); + const { genOpenapiSpec } = await import('../built/gen-spec.js'); const config = loadConfig(); const spec = genOpenapiSpec(config, true); diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs index 3f30e24fb4..7c058a131d 100644 --- a/packages/backend/scripts/measure-memory.mjs +++ b/packages/backend/scripts/measure-memory.mjs @@ -55,7 +55,7 @@ async function getMemoryUsage(pid) { async function measureMemory() { // Start the Misskey backend server using fork to enable IPC - const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], { + const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], { cwd: join(__dirname, '..'), env: { ...process.env, diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 435bd8dd45..adccb4dc3e 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -6,7 +6,7 @@ import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; -import { MeiliSearch } from 'meilisearch'; +import { Meilisearch } from 'meilisearch'; import { MiMeta } from '@/models/Meta.js'; import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; @@ -40,10 +40,10 @@ const $meilisearch: Provider = { useFactory: (config: Config) => { if (config.fulltextSearch?.provider === 'meilisearch') { if (!config.meilisearch) { - throw new Error('MeiliSearch is enabled but no configuration is provided'); + throw new Error('Meilisearch is enabled but no configuration is provided'); } - return new MeiliSearch({ + return new Meilisearch({ host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 268c07582d..25cc7c6797 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -4,16 +4,12 @@ */ import { NestFactory } from '@nestjs/core'; -import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; -import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { NestLogger } from '@/NestLogger.js'; -import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; -import { QueueStatsService } from '@/daemons/QueueStatsService.js'; -import { ServerStatsService } from '@/daemons/ServerStatsService.js'; -import { ServerService } from '@/server/ServerService.js'; -import { MainModule } from '@/MainModule.js'; export async function server() { + const { MainModule } = await import('../MainModule.js'); + const { ServerService } = await import('../server/ServerService.js'); + const app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); @@ -22,6 +18,10 @@ export async function server() { await serverService.launch(); if (process.env.NODE_ENV !== 'test') { + const { ChartManagementService } = await import('../core/chart/ChartManagementService.js'); + const { QueueStatsService } = await import('../daemons/QueueStatsService.js'); + const { ServerStatsService } = await import('../daemons/ServerStatsService.js'); + app.get(ChartManagementService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); @@ -31,6 +31,10 @@ export async function server() { } export async function jobQueue() { + const { QueueProcessorModule } = await import('../queue/QueueProcessorModule.js'); + const { QueueProcessorService } = await import('../queue/QueueProcessorService.js'); + const { ChartManagementService } = await import('../core/chart/ChartManagementService.js'); + const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { logger: new NestLogger(), }); diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 3a33d198a5..6e37bf9e1c 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -13,8 +13,6 @@ import chalk from 'chalk'; import Xev from 'xev'; import Logger from '@/logger.js'; import { envOption } from '../env.js'; -import { masterMain } from './master.js'; -import { workerMain } from './worker.js'; import { readyRef } from './ready.js'; import 'reflect-metadata'; @@ -71,10 +69,12 @@ process.on('exit', code => { if (!envOption.disableClustering) { if (cluster.isPrimary) { logger.info(`Start main process... pid: ${process.pid}`); + const { masterMain } = await import('./master.js'); await masterMain(); ev.mount(); } else if (cluster.isWorker) { logger.info(`Start worker process... pid: ${process.pid}`); + const { workerMain } = await import('./worker.js'); await workerMain(); } else { throw new Error('Unknown process type'); @@ -82,6 +82,7 @@ if (!envOption.disableClustering) { } else { // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない) logger.info(`Start main process... pid: ${process.pid}`); + const { masterMain } = await import('./master.js'); await masterMain(); ev.mount(); } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 6a83359d38..d2b11ef9f4 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -190,6 +190,7 @@ export type Config = { userAgent: string; frontendManifestExists: boolean; frontendEmbedManifestExists: boolean; + rootDir: string; mediaProxy: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; @@ -330,6 +331,7 @@ export function loadConfig(): Config { userAgent: `Misskey/${version} (${config.url})`, frontendManifestExists: frontendManifestExists, frontendEmbedManifestExists: frontendEmbedManifestExists, + rootDir, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 7d60995a7d..855ccc8b98 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -4,27 +4,31 @@ */ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import { Injectable } from '@nestjs/common'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'node:path'; +import { Injectable, Inject } from '@nestjs/common'; import { Mutex } from 'async-mutex'; import fetch from 'node-fetch'; +import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import type { Config } from '@/config.js'; import type { NSFWJS, PredictionType } from 'nsfwjs/core'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma']; let isSupportedCpu: undefined | boolean = undefined; @Injectable() export class AiService { + private readonly modelDir: string; private model: NSFWJS; private modelLoadMutex: Mutex = new Mutex(); constructor( + @Inject(DI.config) + private config: Config, ) { + const md = resolve(this.config.rootDir, 'packages/backend/nsfw-model'); + this.modelDir = md.endsWith('/') ? md : md + '/'; } @bindThis @@ -46,7 +50,7 @@ export class AiService { const nsfw = await import('nsfwjs/core'); await this.modelLoadMutex.runExclusive(async () => { if (this.model == null) { - this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); + this.model = await nsfw.load(pathToFileURL(this.modelDir).toString(), { size: 299 }); } }); } diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 4fb8a93e49..1f2f543962 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -5,29 +5,25 @@ import * as fs from 'node:fs'; import * as Path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const path = Path.resolve(_dirname, '../../../../files'); - @Injectable() export class InternalStorageService { + private readonly path: string; + constructor( @Inject(DI.config) private config: Config, ) { + this.path = Path.resolve(this.config.rootDir, 'files'); } @bindThis public resolvePath(key: string) { - return Path.resolve(path, key); + return Path.resolve(this.path, key); } @bindThis @@ -37,14 +33,14 @@ export class InternalStorageService { @bindThis public saveFromPath(key: string, srcPath: string) { - fs.mkdirSync(path, { recursive: true }); + fs.mkdirSync(this.path, { recursive: true }); fs.copyFileSync(srcPath, this.resolvePath(key)); return `${this.config.url}/files/${key}`; } @bindThis public saveFromBuffer(key: string, data: Buffer) { - fs.mkdirSync(path, { recursive: true }); + fs.mkdirSync(this.path, { recursive: true }); fs.writeFileSync(this.resolvePath(key), data); return `${this.config.url}/files/${key}`; } diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index f102461a50..db58d11e64 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { KEYWORD } from 'color-convert/conversions.js'; +import type { Keyword } from 'color-convert'; @Injectable() export class LoggerService { @@ -15,7 +15,7 @@ export class LoggerService { } @bindThis - public getLogger(domain: string, color?: KEYWORD | undefined) { + public getLogger(domain: string, color?: Keyword | undefined) { return new Logger(domain, color); } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 748f2cbad9..a8f096471e 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -63,20 +63,21 @@ type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { private notifier: { id: MiUser['id']; }; private note: MiNote; - private queue: { + private queue: Map; constructor( private mutingsRepository: MutingsRepository, private notificationService: NotificationService, + private followingsRepository: FollowingsRepository, notifier: { id: MiUser['id']; }, note: MiNote, ) { this.notifier = notifier; this.note = note; - this.queue = []; + this.queue = new Map(); } @bindThis @@ -84,7 +85,7 @@ class NotificationManager { // 自分自身へは通知しない if (this.notifier.id === notifiee) return; - const exist = this.queue.find(x => x.target === notifiee); + const exist = this.queue.get(notifiee); if (exist) { // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする @@ -92,7 +93,7 @@ class NotificationManager { exist.reason = reason; } } else { - this.queue.push({ + this.queue.set(notifiee, { reason: reason, target: notifiee, }); @@ -101,7 +102,50 @@ class NotificationManager { @bindThis public async notify() { - for (const x of this.queue) { + if (this.queue.size === 0) { + return; + } + + let visibleUserIds: Set | null; + + switch (this.note.visibility) { + case 'public': + case 'home': + visibleUserIds = null; + break; + + case 'specified': + visibleUserIds = new Set(this.note.visibleUserIds); + break; + + case 'followers': { + // TODO: フォロワー限定ノートにフォロワーではない人がメンションされた場合通知されるのが正しい挙動なのか確認(一部に挙動の不一致がありそう)。現状は通知されるためフィルタしない + // const targetUserIds = this.queue.map(x => x.target); + // const followers = await this.followingsRepository.find({ + // where: { + // followeeId: this.note.userId, + // followerId: In(targetUserIds), + // isFollowerHibernated: false, + // }, + // select: ['followerId'], + // }); + // visibleUserIds = new Set(followers.map(f => f.followerId)); + visibleUserIds = null; + break; + } + + default: + visibleUserIds = new Set(); + break; + } + + for (const x of this.queue.values()) { + const isVisibleToTarget = visibleUserIds === null || visibleUserIds.has(x.target); + + if (!isVisibleToTarget) { + continue; + } + if (x.reason === 'renote') { this.notificationService.createNotification(x.target, 'renote', { noteId: this.note.id, @@ -772,7 +816,7 @@ export class NoteCreateService implements OnApplicationShutdown { this.webhookService.enqueueUserWebhook(user.id, 'note', { note: noteObj }); - const nm = new NotificationManager(this.mutingsRepository, this.notificationService, user, note); + const nm = new NotificationManager(this.mutingsRepository, this.notificationService, this.followingsRepository, user, note); await this.createMentionedEvents(mentionedUsers, note, nm); diff --git a/packages/backend/src/core/NotificationService.ts b/packages/backend/src/core/NotificationService.ts index b3a9c6f35a..fb92f196da 100644 --- a/packages/backend/src/core/NotificationService.ts +++ b/packages/backend/src/core/NotificationService.ts @@ -247,7 +247,8 @@ export class NotificationService implements OnApplicationShutdown { private toXListId(id: string): string { const { date, additional } = this.idService.parseFull(id); - return date.toString() + '-' + additional.toString(); + // Redis Stream sequenceはunit64制約があるため、収まらない場合は下位64bitを取る + return date.toString() + '-' + BigInt.asUintN(64, additional).toString(); } @bindThis diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 9120de1f9f..d96d6c70d0 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -91,13 +91,27 @@ export class RelayService { return JSON.stringify(result); } + @bindThis + private getAcceptedRelays(): Promise { + return this.relaysCache.fetch(() => this.relaysRepository.findBy({ + status: 'accepted', + })); + } + + @bindThis + public async isRelayActor(actor: { inbox: string | null; sharedInbox: string | null }): Promise { + const relays = await this.getAcceptedRelays(); + return relays.some(relay => + (actor.inbox != null && relay.inbox === actor.inbox) + || (actor.sharedInbox != null && relay.inbox === actor.sharedInbox), + ); + } + @bindThis public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise { if (activity == null) return; - const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ - status: 'accepted', - })); + const relays = await this.getAcceptedRelays(); if (relays.length === 0) return; const copy = deepClone(activity); diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 2ffee69c21..4515cfd29c 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -533,7 +533,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { roleId: In(administratorRoles.map(r => r.id)), }) : []; // TODO: isRootなアカウントも含める - return assigns.map(a => a.userId); + // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) + return [...new Set(assigns.map(a => a.userId))].sort((x, y) => x.localeCompare(y)); } @bindThis diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 9ea6a27295..df7ff311d2 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -17,7 +17,7 @@ import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { Index, MeiliSearch } from 'meilisearch'; +import type { Index, Meilisearch } from 'meilisearch'; type K = string; type V = string | number | boolean; @@ -85,7 +85,7 @@ export class SearchService { private config: Config, @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, + private meilisearch: Meilisearch | null, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -187,7 +187,7 @@ export class SearchService { return this.searchNoteByLike(q, me, opts, pagination); } case 'meilisearch': { - return this.searchNoteByMeiliSearch(q, me, opts, pagination); + return this.searchNoteByMeilisearch(q, me, opts, pagination); } default: { const _: never = this.provider; @@ -239,14 +239,14 @@ export class SearchService { } @bindThis - private async searchNoteByMeiliSearch( + private async searchNoteByMeilisearch( q: string, me: MiUser | null, opts: SearchOpts, pagination: SearchPagination, ): Promise { if (!this.meilisearch || !this.meilisearchNoteIndex) { - throw new Error('MeiliSearch is not available'); + throw new Error('Meilisearch is not available'); } const filter: Q = { diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index a85da62b86..bb1b8f9f3a 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -164,4 +164,3 @@ export class SignupService { return { account, secret }; } } - diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index ff47ca930d..27ab0e3447 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -259,7 +259,7 @@ export class ApInboxService { @bindThis private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { + if (actor.uri !== getApId(activity.actor)) { return 'invalid actor'; } @@ -302,12 +302,14 @@ export class ApInboxService { @bindThis private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise { - const uri = getApId(activity); - if (actor.isSuspended) { return; } + // リレーからのAnnounceかチェック + const fromRelay = await this.relayService.isRelayActor(actor); + const uri = getApId(fromRelay ? target : activity); + // アナウンス先が許可されているかチェック if (!this.utilityService.isFederationAllowedUri(uri)) return; @@ -336,6 +338,14 @@ export class ApInboxService { throw err; } + // リレーからのAnnounceはリノートを作成せず、ノートを直接公開する + if (fromRelay) { + this.logger.info(`Publishing relay-delivered note: ${uri}`); + const noteObj = await this.noteEntityService.pack(renote, null, { skipHide: true, withReactionAndUserPairCache: true }); + this.globalEventService.publishNotesStream(noteObj); + return; + } + if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { return 'skip: invalid actor for this activity'; } @@ -459,7 +469,7 @@ export class ApInboxService { @bindThis private async delete(actor: MiRemoteUser, activity: IDelete): Promise { - if (actor.uri !== activity.actor) { + if (actor.uri !== getApId(activity.actor)) { return 'invalid actor'; } @@ -613,7 +623,7 @@ export class ApInboxService { @bindThis private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { + if (actor.uri !== getApId(activity.actor)) { return 'invalid actor'; } @@ -633,7 +643,7 @@ export class ApInboxService { @bindThis private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { + if (actor.uri !== getApId(activity.actor)) { return 'invalid actor'; } @@ -767,7 +777,7 @@ export class ApInboxService { @bindThis private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { - if (actor.uri !== activity.actor) { + if (actor.uri !== getApId(activity.actor)) { return 'skip: invalid actor'; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index ebe8e9c964..39396cb741 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -376,7 +376,7 @@ export class ApPersonService implements OnModuleInit { isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo, movedAt: person.movedTo ? new Date() : null, - alsoKnownAs: person.alsoKnownAs, + alsoKnownAs: toArray(person.alsoKnownAs), isExplorable: person.discoverable, username: person.preferredUsername, usernameLower: person.preferredUsername?.toLowerCase(), @@ -568,7 +568,7 @@ export class ApPersonService implements OnModuleInit { isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, movedToUri: person.movedTo ?? null, - alsoKnownAs: person.alsoKnownAs ?? null, + alsoKnownAs: person.alsoKnownAs ? toArray(person.alsoKnownAs) : null, isExplorable: person.discoverable, ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), } as Partial & Pick; diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 8e56ddbc02..b9b656bd10 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -132,7 +132,7 @@ export class MetaEntityService { sentryForFrontend: this.config.sentryForFrontend ?? null, mediaProxy: this.config.mediaProxy, enableUrlPreview: instance.urlPreviewEnabled, - noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local', + noteSearchableScope: (this.config.fulltextSearch?.provider === 'meilisearch' && this.config.meilisearch?.scope === 'local') ? 'local' : 'global', maxFileSize: this.config.maxFileSize, federation: this.meta.federation, }; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 0f4051e7b8..996f0bad2e 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -51,6 +51,7 @@ import { ChatService } from '@/core/ChatService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; +import { toArray } from '@/misc/prelude/array.js'; const Ajv = _Ajv.default; const ajv = new Ajv(); @@ -527,10 +528,10 @@ export class UserEntityService implements OnModuleInit { url: profile!.url, uri: user.uri, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, - alsoKnownAs: user.alsoKnownAs - ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) - .then(xs => xs.length === 0 ? null : xs.filter(x => x != null)) - : null, + alsoKnownAs: user.alsoKnownAs ? + Promise.all(toArray(user.alsoKnownAs).map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) + .then(xs => xs.length === 0 ? null : xs.filter(x => x != null)) + : null, createdAt: this.idService.parse(user.id).date.toISOString(), updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null, diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index ff5363a425..ce76f8d05e 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -9,11 +9,11 @@ import { default as convertColor } from 'color-convert'; import { format as dateFormat } from 'date-fns'; import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; -import type { KEYWORD } from 'color-convert/conversions.js'; +import type { Keyword } from 'color-convert'; type Context = { name: string; - color?: KEYWORD; + color?: Keyword; }; type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; @@ -23,7 +23,7 @@ export default class Logger { private context: Context; private parentLogger: Logger | null = null; - constructor(context: string, color?: KEYWORD) { + constructor(context: string, color?: Keyword) { this.context = { name: context, color: color, @@ -31,7 +31,7 @@ export default class Logger { } @bindThis - public createSubLogger(context: string, color?: KEYWORD): Logger { + public createSubLogger(context: string, color?: Keyword): Logger { const logger = new Logger(context, color); logger.parentLogger = this; return logger; diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index 8b81702d19..291a33385f 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -5,12 +5,19 @@ // Crockford's Base32 // https://github.com/ulid/spec#encoding -import { parseBigInt32 } from '@/misc/bigint.js'; const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; +function parseBigIntCrockford(str: string): bigint { + let result = 0n; + for (let i = 0; i < str.length; i++) { + result = result * 32n + BigInt(CHARS.indexOf(str[i])); + } + return result; +} + function parseBase32(timestamp: string) { let time = 0; for (let i = 0; i < timestamp.length; i++) { @@ -26,6 +33,6 @@ export function parseUlid(id: string): { date: Date; } { export function parseUlidFull(id: string): { date: number; additional: bigint; } { return { date: parseBase32(id.slice(0, 10)), - additional: parseBigInt32(id.slice(10, 26)), + additional: parseBigIntCrockford(id.slice(10, 26)), }; } diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 13f0b05667..d14a210392 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -36,4 +36,9 @@ export class MiAvatarDecoration { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisDecoration: string[]; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 079e014da8..0e4cd59888 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -13,7 +13,7 @@ import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataServic import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; -import { getApId } from '@/core/activitypub/type.js'; +import { getApId, isActor, isDelete } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; @@ -84,6 +84,23 @@ export class InboxProcessorService implements OnApplicationShutdown { return `Old keyId is no longer supported. ${keyIdLower}`; } + { + let userExistenceCheckApId: string | null = null; + + // 存在しないActorに対するActorのDeleteアクティビティは無視する。 + // actorとobjectが同じならばそれはActorに違いない + if (isDelete(activity) && typeof activity.object === 'object' && (isActor(activity.object) || getApId(activity.actor) === getApId(activity.object))) { + userExistenceCheckApId = getApId(activity.object); + } + + if (userExistenceCheckApId != null) { + const user = await this.apDbResolverService.getUserFromApId(userExistenceCheckApId); + if (user == null) { + return `skip: user not found for delete activity. ${getApId(userExistenceCheckApId)}`; + } + } + } + // HTTP-Signature keyIdを元にDBから取得 let authUser: { user: MiRemoteUser; @@ -98,9 +115,9 @@ export class InboxProcessorService implements OnApplicationShutdown { // 対象が4xxならスキップ if (err instanceof StatusError) { if (!err.isRetryable) { - throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); + throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${getApId(activity.actor)} - ${err.statusCode}`); } - throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); + throw new Error(`Error in actor ${getApId(activity.actor)} - ${err.statusCode}`); } } } @@ -119,7 +136,7 @@ export class InboxProcessorService implements OnApplicationShutdown { const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); // また、signatureのsignerは、activity.actorと一致する必要がある - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { + if (!httpSignatureValidated || authUser.user.uri !== getApId(activity.actor)) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る const ldSignature = activity.signature; if (ldSignature) { @@ -170,8 +187,8 @@ export class InboxProcessorService implements OnApplicationShutdown { //#endregion // もう一度actorチェック - if (authUser.user.uri !== activity.actor) { - throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`); + if (authUser.user.uri !== getApId(activity.actor)) { + throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`); } const ldHost = this.utilityService.extractDbHost(authUser.user.uri); @@ -226,14 +243,17 @@ export class InboxProcessorService implements OnApplicationShutdown { } } catch (e) { if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { - return 'blocked notes with prohibited words'; - } - if (e.id === '85ab9bd7-3a41-4530-959d-f07073900109') { - return 'actor has been suspended'; - } - if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note - return e.message; + switch (e.id) { + case '689ee33f-f97c-479a-ac49-1b9f8140af99': + return 'blocked notes with prohibited words'; + case '85ab9bd7-3a41-4530-959d-f07073900109': + return 'actor has been suspended'; + case 'd450b8a9-48e4-4dab-ae36-f4db763fda7c': // invalid Note + return e.message; + case '9f466dab-c856-48cd-9e65-ff90ff750580': + return 'note contains too many mentions'; + case '09d79f9e-64f1-4316-9cfa-e75c4d091574': // Instance is blocked + return 'skip: blocked instance'; } } throw e; diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index f5034d0733..4a5ac799ad 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -4,8 +4,7 @@ */ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; +import { resolve } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import type { Config } from '@/config.js'; import type { DriveFilesRepository } from '@/models/_.js'; @@ -25,11 +24,6 @@ import { FileServerFileResolver } from './file/FileServerFileResolver.js'; import { FileServerProxyHandler } from './file/FileServerProxyHandler.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const assets = `${_dirname}/../../server/file/assets/`; - @Injectable() export class FileServerService { private logger: Logger; @@ -37,6 +31,8 @@ export class FileServerService { private proxyHandler: FileServerProxyHandler; private fileResolver: FileServerFileResolver; + private readonly assets: string; + constructor( @Inject(DI.config) private config: Config, @@ -52,6 +48,7 @@ export class FileServerService { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('server', 'gray'); + this.assets = resolve(this.config.rootDir, 'packages/backend/src/server/file/assets'); this.fileResolver = new FileServerFileResolver( this.driveFilesRepository, this.fileInfoService, @@ -61,13 +58,13 @@ export class FileServerService { this.driveHandler = new FileServerDriveHandler( this.config, this.fileResolver, - assets, + this.assets, this.videoProcessingService, ); this.proxyHandler = new FileServerProxyHandler( this.config, this.fileResolver, - assets, + this.assets, this.imageProcessingService, ); @@ -87,7 +84,7 @@ export class FileServerService { fastify.register((fastify, options, done) => { fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); fastify.get('/files/app-default.jpg', (request, reply) => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + const file = fs.createReadStream(`${this.assets}/dummy.png`); reply.header('Content-Type', 'image/jpeg'); reply.header('Cache-Control', 'max-age=31536000, immutable'); return reply.send(file); @@ -121,7 +118,7 @@ export class FileServerService { reply.header('Cache-Control', 'max-age=300'); if (request.query && 'fallback' in request.query) { - return reply.sendFile('/dummy.png', assets); + return reply.sendFile('/dummy.png', this.assets); } if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) { diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts index 5980609f02..7c9710c693 100644 --- a/packages/backend/src/server/HealthServerService.ts +++ b/packages/backend/src/server/HealthServerService.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import { readyRef } from '@/boot/ready.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import type { MeiliSearch } from 'meilisearch'; +import type { Meilisearch } from 'meilisearch'; @Injectable() export class HealthServerService { @@ -34,7 +34,7 @@ export class HealthServerService { private db: DataSource, @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, + private meilisearch: Meilisearch | null, ) {} @bindThis diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index 0121c302ac..baa87dbbbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -55,6 +55,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -68,6 +72,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['name', 'description', 'url'], } as const; @@ -84,6 +89,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); return { @@ -94,6 +100,7 @@ export default class extends Endpoint { // eslint- description: created.description, url: created.url, roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration, + category: created.category, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index 765bfd6766..7be3d79fee 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -60,6 +60,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -95,6 +99,7 @@ export default class extends Endpoint { // eslint- description: avatarDecoration.description, url: avatarDecoration.url, roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, + category: avatarDecoration.category, })); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 22476a6888..b84b4c5085 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -30,6 +30,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['id'], } as const; @@ -45,6 +46,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..58610c76b4 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -119,7 +119,7 @@ export default class extends Endpoint { // eslint- } // Update - this.driveFoldersRepository.update(folder.id, { + await this.driveFoldersRepository.update(folder.id, { name: folder.name, parentId: folder.parentId, }); diff --git a/packages/backend/src/server/api/endpoints/endpoint.ts b/packages/backend/src/server/api/endpoints/endpoint.ts index fe7e9c36f3..872346e560 100644 --- a/packages/backend/src/server/api/endpoints/endpoint.ts +++ b/packages/backend/src/server/api/endpoints/endpoint.ts @@ -5,7 +5,13 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import endpoints from '../endpoints.js'; + +// 循環参照を回避 +let endpointsPromise: Promise | undefined; + +function getEndpoints() { + return endpointsPromise ??= import('../endpoints.js').then(module => module.default); +} export const meta = { requireCredential: false, @@ -43,6 +49,7 @@ export default class extends Endpoint { // eslint- constructor( ) { super(meta, paramDef, async (ps) => { + const endpoints = await getEndpoints(); const ep = endpoints.find(x => x.name === ps.endpoint); if (ep == null) return null; return { diff --git a/packages/backend/src/server/api/endpoints/endpoints.ts b/packages/backend/src/server/api/endpoints/endpoints.ts index 4aedf62a84..0837b2d4e9 100644 --- a/packages/backend/src/server/api/endpoints/endpoints.ts +++ b/packages/backend/src/server/api/endpoints/endpoints.ts @@ -5,7 +5,13 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import endpoints from '../endpoints.js'; + +// 循環参照を回避 +let endpointsPromise: Promise | undefined; + +function getEndpoints() { + return endpointsPromise ??= import('../endpoints.js').then(module => module.default); +} export const meta = { requireCredential: false, @@ -39,6 +45,7 @@ export default class extends Endpoint { // eslint- constructor( ) { super(meta, paramDef, async () => { + const endpoints = await getEndpoints(); return endpoints.map(x => x.name); }); } diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..ca0a5e2e25 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -49,6 +49,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -76,6 +80,7 @@ export default class extends Endpoint { // eslint- description: decoration.description, url: decoration.url, roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)), + category: decoration.category, })); }); } diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 6097f9c562..629214382b 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -8,7 +8,7 @@ process.env.NODE_ENV = 'test'; import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; -import { describe, test, expect } from '@jest/globals'; +import { describe, test, expect } from 'vitest'; import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; import { paramDef } from './create.js'; diff --git a/packages/backend/src/server/api/endpoints/users/show.test.ts b/packages/backend/src/server/api/endpoints/users/show.test.ts index 068ffd8bc9..7d8f44592d 100644 --- a/packages/backend/src/server/api/endpoints/users/show.test.ts +++ b/packages/backend/src/server/api/endpoints/users/show.test.ts @@ -5,6 +5,7 @@ process.env.NODE_ENV = 'test'; +import { describe, test, expect } from 'vitest'; import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; import { paramDef } from './show.js'; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 24bc619e79..9990d57f2b 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -4,9 +4,7 @@ */ import { randomUUID } from 'node:crypto'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import * as fs from 'node:fs'; +import { resolve } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import sharp from 'sharp'; @@ -67,35 +65,17 @@ import { ErrorPage } from './views/error.js'; import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -let rootDir = _dirname; -// 見つかるまで上に遡る -while (!fs.existsSync(resolve(rootDir, 'packages'))) { - const parentDir = dirname(rootDir); - if (parentDir === rootDir) { - throw new Error('Cannot find root directory'); - } - rootDir = parentDir; -} - -const backendRootDir = resolve(rootDir, 'packages/backend'); -const frontendRootDir = resolve(rootDir, 'packages/frontend'); - -const staticAssets = resolve(backendRootDir, 'assets'); -const clientAssets = resolve(frontendRootDir, 'assets'); -const assets = resolve(rootDir, 'built/_frontend_dist_'); -const swAssets = resolve(rootDir, 'built/_sw_dist_'); -const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist'); -const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg'); -const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_'); -const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_'); -const tarball = resolve(rootDir, 'built/tarball'); - @Injectable() export class ClientServerService { - private logger: Logger; + private readonly staticAssets: string; + private readonly clientAssets: string; + private readonly assets: string; + private readonly swAssets: string; + private readonly fluentEmojisDir: string; + private readonly twemojiDir: string; + private readonly frontendViteOut: string; + private readonly frontendEmbedViteOut: string; + private readonly tarball: string; constructor( @Inject(DI.config) @@ -149,6 +129,17 @@ export class ClientServerService { private clientLoggerService: ClientLoggerService, ) { //this.createServer = this.createServer.bind(this); + const backendRootdir = resolve(this.config.rootDir, 'packages/backend'); + const frontendRootdir = resolve(this.config.rootDir, 'packages/frontend'); + this.staticAssets = resolve(backendRootdir, 'assets'); + this.clientAssets = resolve(frontendRootdir, 'assets'); + this.assets = resolve(this.config.rootDir, 'built/_frontend_dist_'); + this.swAssets = resolve(this.config.rootDir, 'built/_sw_dist_'); + this.fluentEmojisDir = resolve(this.config.rootDir, 'fluent-emojis/dist'); + this.twemojiDir = resolve(backendRootdir, 'node_modules/@discordapp/twemoji/dist/svg'); + this.frontendViteOut = resolve(this.config.rootDir, 'built/_frontend_vite_'); + this.frontendEmbedViteOut = resolve(this.config.rootDir, 'built/_frontend_embed_vite_'); + this.tarball = resolve(this.config.rootDir, 'built/tarball'); } @bindThis @@ -223,17 +214,17 @@ export class ClientServerService { //#region vite assets if (this.config.frontendEmbedManifestExists) { - console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`); + this.clientLoggerService.logger.info(`[ClientServerService] Using built frontend vite assets. ${this.frontendViteOut}`); fastify.register((fastify, options, done) => { fastify.register(fastifyStatic, { - root: frontendViteOut, + root: this.frontendViteOut, prefix: '/vite/', maxAge: ms('30 days'), immutable: true, decorateReply: false, }); fastify.register(fastifyStatic, { - root: frontendEmbedViteOut, + root: this.frontendEmbedViteOut, prefix: '/embed_vite/', maxAge: ms('30 days'), immutable: true, @@ -265,21 +256,21 @@ export class ClientServerService { //#region static assets fastify.register(fastifyStatic, { - root: staticAssets, + root: this.staticAssets, prefix: '/static-assets/', maxAge: ms('7 days'), decorateReply: false, }); fastify.register(fastifyStatic, { - root: clientAssets, + root: this.clientAssets, prefix: '/client-assets/', maxAge: ms('7 days'), decorateReply: false, }); fastify.register(fastifyStatic, { - root: assets, + root: this.assets, prefix: '/assets/', maxAge: ms('7 days'), decorateReply: false, @@ -287,7 +278,7 @@ export class ClientServerService { fastify.register((fastify, options, done) => { fastify.register(fastifyStatic, { - root: tarball, + root: this.tarball, prefix: '/tarball/', maxAge: ms('30 days'), immutable: true, @@ -298,11 +289,11 @@ export class ClientServerService { }); fastify.get('/favicon.ico', async (request, reply) => { - return reply.sendFile('/favicon.ico', staticAssets); + return reply.sendFile('/favicon.ico', this.staticAssets); }); fastify.get('/apple-touch-icon.png', async (request, reply) => { - return reply.sendFile('/apple-touch-icon.png', staticAssets); + return reply.sendFile('/apple-touch-icon.png', this.staticAssets); }); fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { @@ -315,7 +306,7 @@ export class ClientServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - return reply.sendFile(path, fluentEmojisDir, { + return reply.sendFile(path, this.fluentEmojisDir, { maxAge: ms('30 days'), }); }); @@ -330,7 +321,7 @@ export class ClientServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - return reply.sendFile(path, twemojiDir, { + return reply.sendFile(path, this.twemojiDir, { maxAge: ms('30 days'), }); }); @@ -344,7 +335,7 @@ export class ClientServerService { } const mask = await sharp( - `${twemojiDir}/${path.replace('.png', '')}.svg`, + `${this.twemojiDir}/${path.replace('.png', '')}.svg`, { density: 1000 }, ) .resize(488, 488) @@ -380,7 +371,7 @@ export class ClientServerService { // ServiceWorker fastify.get('/sw.js', async (request, reply) => { - return await reply.sendFile('/sw.js', swAssets, { + return await reply.sendFile('/sw.js', this.swAssets, { maxAge: ms('10 minutes'), }); }); @@ -390,13 +381,40 @@ export class ClientServerService { // Embed Javascript fastify.get('/embed.js', async (request, reply) => { - return await reply.sendFile('/embed.js', staticAssets, { + return await reply.sendFile('/embed.js', this.staticAssets, { maxAge: ms('1 day'), }); }); fastify.get('/robots.txt', async (request, reply) => { - return await reply.sendFile('/robots.txt', staticAssets); + const disallowedPaths = [ + '/settings', + '/admin', + '/custom-emojis-manager', + '/avatar-decorations', + '/share', + '/my', + '/api', + '/inbox', + '/oauth', + '/proxy', + '/url', + ]; + + if (this.meta.ugcVisibilityForVisitor === 'none') { + disallowedPaths.push( + '/@', + '/notes', + ); + } + + let content = `User-agent: *\n`; + content += disallowedPaths.map((path) => `Disallow: ${path}`).join('\n') + '\n'; + content += 'Allow: /\n'; + content += '\n# todo: sitemap\n'; + + reply.header('Content-Type', 'text/plain; charset=utf-8'); + return await reply.send(content); }); // OpenSearch XML diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 36272c81d5..2859b2b985 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -3,9 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { promises as fsp, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { promises as fsp } from 'node:fs'; import { languages } from 'i18n/const'; import { Injectable, Inject } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; @@ -18,25 +17,11 @@ import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; import type { CommonData, ViteFiles } from './views/_.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -let rootDir = _dirname; -// 見つかるまで上に遡る -while (!existsSync(resolve(rootDir, 'packages'))) { - const parentDir = dirname(rootDir); - if (parentDir === rootDir) { - throw new Error('Cannot find root directory'); - } - rootDir = parentDir; -} - -const frontendViteBuilt = resolve(rootDir, 'built/_frontend_vite_'); -const frontendEmbedViteBuilt = resolve(rootDir, 'built/_frontend_embed_vite_'); - @Injectable() export class HtmlTemplateService { private frontendAssetsFetched = false; + private readonly frontendViteBuilt: string; + private readonly frontendEmbedViteBuilt: string; public frontendViteFiles: ViteFiles | null = null; public frontendBootloaderJs: string | null = null; public frontendBootloaderCss: string | null = null; @@ -53,6 +38,8 @@ export class HtmlTemplateService { private metaEntityService: MetaEntityService, ) { + this.frontendViteBuilt = resolve(this.config.rootDir, 'built/_frontend_vite_'); + this.frontendEmbedViteBuilt = resolve(this.config.rootDir, 'built/_frontend_embed_vite_'); } // 初期ロードで読み込むべきファイルのパスを収集する。 @@ -118,22 +105,22 @@ export class HtmlTemplateService { embedBootJs, embedBootCss, ] = await Promise.all([ - fsp.readFile(resolve(frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), ]); let feViteManifest: Manifest | null = null; let embedFeViteManifest: Manifest | null = null; if (this.config.frontendManifestExists) { - const manifestContent = await fsp.readFile(resolve(frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + const manifestContent = await fsp.readFile(resolve(this.frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); feViteManifest = manifestContent ? JSON.parse(manifestContent) : null; } if (this.config.frontendEmbedManifestExists) { - const manifestContent = await fsp.readFile(resolve(frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + const manifestContent = await fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null; } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index bd1dbb430c..5275a90ec3 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; +import type { SummalyResult } from '@misskey-dev/summaly'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml index 4d1b4b0d60..53e297f867 100644 --- a/packages/backend/test-federation/compose.yml +++ b/packages/backend/test-federation/compose.yml @@ -43,16 +43,12 @@ services: target: /misskey/packages/backend/test-federation/test read_only: true - type: bind - source: ../jest.config.cjs - target: /misskey/packages/backend/jest.config.cjs + source: ../vitest.config.ts + target: /misskey/packages/backend/vitest.config.ts read_only: true - type: bind - source: ../jest.config.fed.cjs - target: /misskey/packages/backend/jest.config.fed.cjs - read_only: true - - type: bind - source: ../jest.js - target: /misskey/packages/backend/jest.js + source: ../vitest.config.fed.ts + target: /misskey/packages/backend/vitest.config.fed.ts read_only: true - type: bind source: ../scripts/compile_config.js diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts index ddc8e4f9d0..44342e7743 100644 --- a/packages/backend/test-federation/test/abuse-report.test.ts +++ b/packages/backend/test-federation/test/abuse-report.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import { rejects, strictEqual } from 'node:assert'; import * as Misskey from 'misskey-js'; import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js'; diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts index ef910eeaea..0cc5ca046b 100644 --- a/packages/backend/test-federation/test/block.test.ts +++ b/packages/backend/test-federation/test/block.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import { deepStrictEqual, rejects, strictEqual } from 'node:assert'; import * as Misskey from 'misskey-js'; import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts index f755183b4d..8c392148fc 100644 --- a/packages/backend/test-federation/test/drive.test.ts +++ b/packages/backend/test-federation/test/drive.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import assert, { strictEqual } from 'node:assert'; import * as Misskey from 'misskey-js'; import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js'; diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts index 3119ca6e4d..308323a7ce 100644 --- a/packages/backend/test-federation/test/emoji.test.ts +++ b/packages/backend/test-federation/test/emoji.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import assert, { deepStrictEqual, strictEqual } from 'assert'; import * as Misskey from 'misskey-js'; import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js'; diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts index 56a57de8a4..0801ce84ee 100644 --- a/packages/backend/test-federation/test/move.test.ts +++ b/packages/backend/test-federation/test/move.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import assert, { strictEqual } from 'node:assert'; import { createAccount, type LoginUser, sleep } from './utils.js'; diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts index a339cd86d2..d9556b0e93 100644 --- a/packages/backend/test-federation/test/note.test.ts +++ b/packages/backend/test-federation/test/note.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll, afterAll } from 'vitest'; import assert, { rejects, strictEqual } from 'node:assert'; import * as Misskey from 'misskey-js'; import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js'; @@ -214,7 +215,7 @@ describe('Note', () => { * @see https://github.com/misskey-dev/misskey/issues/15548 */ describe('To only resolved and not followed user', () => { - test.failing('Check', async () => { + test.skip('Check', async () => { const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote; const noteInA = await resolveRemoteNote('b.test', note.id, alice); await sleep(); @@ -254,7 +255,7 @@ describe('Note', () => { * FIXME: implement soft deletion as well as user? * @see https://github.com/misskey-dev/misskey/issues/11437 */ - test.failing('Not found even if resolve again', async () => { + test.skip('Not found even if resolve again', async () => { const noteInB = await resolveRemoteNote('a.test', note.id, bob); await rejects( async () => await bob.client.request('notes/show', { noteId: noteInB.id }), diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts index 6d55353653..7058da8a42 100644 --- a/packages/backend/test-federation/test/notification.test.ts +++ b/packages/backend/test-federation/test/notification.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll, afterAll } from 'vitest'; import * as Misskey from 'misskey-js'; import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts index 00635e654b..191bd416b9 100644 --- a/packages/backend/test-federation/test/timeline.test.ts +++ b/packages/backend/test-federation/test/timeline.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll, afterAll } from 'vitest'; import { strictEqual } from 'assert'; import * as Misskey from 'misskey-js'; import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js'; @@ -117,7 +118,7 @@ describe('Timeline', () => { * FIXME: can receive this * @see https://github.com/misskey-dev/misskey/issues/14083 */ - test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => { + test.skip('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => { await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' }); }); @@ -125,7 +126,7 @@ describe('Timeline', () => { * FIXME: cannot receive this * @see https://github.com/misskey-dev/misskey/issues/14084 */ - test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => { + test.skip('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => { const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote; await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] }); }); diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts index ebbe9ff5ba..c6a93c2dd5 100644 --- a/packages/backend/test-federation/test/user.test.ts +++ b/packages/backend/test-federation/test/user.test.ts @@ -1,3 +1,4 @@ +import { describe, test, beforeAll } from 'vitest'; import assert, { rejects, strictEqual } from 'node:assert'; import * as Misskey from 'misskey-js'; import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js'; diff --git a/packages/backend/test-server/.swcrc b/packages/backend/test-server/.swcrc deleted file mode 100644 index 3859603da3..0000000000 --- a/packages/backend/test-server/.swcrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://swc.rs/schema.json", - "jsc": { - "parser": { - "syntax": "typescript", - "dynamicImport": true, - "decorators": true - }, - "transform": { - "legacyDecorator": true, - "decoratorMetadata": true - }, - "experimental": { - "keepImportAssertions": true - }, - "baseUrl": "../src-js", - "paths": { - "@/*": ["*"] - }, - "target": "es2022" - }, - "minify": false -} diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts index 04bf62d209..5abe8dd296 100644 --- a/packages/backend/test-server/entry.ts +++ b/packages/backend/test-server/entry.ts @@ -19,7 +19,7 @@ let serverService: ServerService; /** * テスト用のサーバインスタンスを起動する */ -async function launch() { +export async function setup() { await killTestServer(); console.log('starting application...'); @@ -38,6 +38,15 @@ async function launch() { console.log('application initialized.'); } +/** + * テスト用のサーバインスタンスを停止する + */ +export async function teardown() { + await serverService.dispose(); + await app.close(); + await killTestServer(); +} + /** * 既に重複したポートで待ち受けしているサーバがある場合はkillする */ @@ -94,5 +103,3 @@ async function startControllerEndpoints(port = config.port + 1000) { await fastify.listen({ port: port, host: 'localhost' }); } - -export default launch; diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json index 7ed7c10ed7..33978ec95f 100644 --- a/packages/backend/test-server/tsconfig.json +++ b/packages/backend/test-server/tsconfig.json @@ -25,8 +25,6 @@ "isolatedModules": true, "jsx": "react-jsx", "jsxImportSource": "@kitajs/html", - "rootDir": "../src", - "baseUrl": "./", "paths": { "@/*": ["../src/*"] }, diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts index 48e1bababb..bd529067cc 100644 --- a/packages/backend/test/e2e/2fa.ts +++ b/packages/backend/test/e2e/2fa.ts @@ -20,6 +20,7 @@ import type { RegistrationResponseJSON, } from '@simplewebauthn/types'; import type * as misskey from 'misskey-js'; +import { describe, beforeAll, test } from 'vitest'; describe('2要素認証', () => { let alice: misskey.entities.SignupResponse; diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 70a5c9579e..ea7cd77d66 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, beforeEach, test } from 'vitest'; import { api, failedApiCall, diff --git a/packages/backend/test/e2e/api-visibility.ts b/packages/backend/test/e2e/api-visibility.ts index 2dd645d97a..4f244c0cce 100644 --- a/packages/backend/test/e2e/api-visibility.ts +++ b/packages/backend/test/e2e/api-visibility.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, beforeEach, test } from 'vitest'; import { UserToken, api, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index 49c6a0636b..5eb7934c1a 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -6,7 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { IncomingMessage } from 'http'; +import { describe, beforeAll, test } from 'vitest'; +import { IncomingMessage } from 'node:http'; import { api, connectStream, diff --git a/packages/backend/test/e2e/block.ts b/packages/backend/test/e2e/block.ts index 35b0e59383..9ef4dd8be9 100644 --- a/packages/backend/test/e2e/block.ts +++ b/packages/backend/test/e2e/block.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { api, castAsError, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/clips.ts b/packages/backend/test/e2e/clips.ts index fe9a217ee8..465089db2e 100644 --- a/packages/backend/test/e2e/clips.ts +++ b/packages/backend/test/e2e/clips.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, beforeEach, afterEach, test } from 'vitest'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js'; import type * as Misskey from 'misskey-js'; @@ -176,7 +177,9 @@ describe('クリップ', () => { { label: 'descriptionがnull', parameters: { description: null } }, { label: 'descriptionが最大長', parameters: { description: 'a'.repeat(2048) } }, ]; - test.each(createClipAllowedPattern)('の作成は$labelでもできる', async ({ parameters }) => await create(parameters)); + test.each(createClipAllowedPattern)('の作成は$labelでもできる', async ({ parameters }) => { + await create(parameters); + }); const createClipDenyPattern = [ { label: 'nameがnull', parameters: { name: null } }, @@ -233,11 +236,13 @@ describe('クリップ', () => { assert.strictEqual(res.isFavorited, false); }); - test.each(createClipAllowedPattern)('の更新は$labelでもできる', async ({ parameters }) => await update({ - clipId: (await create()).id, - name: 'updated', - ...parameters, - })); + test.each(createClipAllowedPattern)('の更新は$labelでもできる', async ({ parameters }) => { + await update({ + clipId: (await create()).id, + name: 'updated', + ...parameters, + }); + }); test.each([ { label: 'clipIdがnull', parameters: { clipId: null } }, diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts index 43a73163eb..5b92abba16 100644 --- a/packages/backend/test/e2e/drive.ts +++ b/packages/backend/test/e2e/drive.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { api, makeStreamCatcher, post, signup, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index 469f19e2b9..09198384c4 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -6,10 +6,11 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test, expect } from 'vitest'; // node-fetch only supports it's own Blob yet // https://github.com/node-fetch/node-fetch/pull/1664 import { Blob } from 'node-fetch'; -import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js'; +import { api, castAsError, initTestDb, post, role, signup, simpleGet, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; import { MiUser } from '@/models/_.js'; @@ -581,6 +582,30 @@ describe('Endpoints', () => { }); describe('drive/files/create', () => { + const assignRole = async (userId: string, policies: Record) => { + const createdRole = await role(alice, {}, policies); + + const assign = await api('admin/roles/assign', { + userId, + roleId: createdRole.id, + }, alice); + + assert.strictEqual(assign.status, 204); + + return createdRole; + }; + + const cleanupRole = async (userId: string, roleId: string) => { + await api('admin/roles/unassign', { + userId, + roleId, + }, alice); + + await api('admin/roles/delete', { + roleId, + }, alice); + }; + test('ファイルを作成できる', async () => { const res = await uploadFile(alice); @@ -659,6 +684,104 @@ describe('Endpoints', () => { assert.strictEqual(webpublicType, 'image/webp'); }); } + + test('uploadableFileTypes が */* なら任意のファイルをアップロードできる', async () => { + const createdRole = await assignRole(bob.id, { + uploadableFileTypes: { + useDefault: false, + priority: 1, + value: ['*/*'], + }, + }); + + try { + const res = await uploadFile(bob, { + blob: new Blob([new Uint8Array(10)]), + }); + + assert.strictEqual(res.status, 200); + } finally { + await cleanupRole(bob.id, createdRole.id); + } + }); + + test('uploadableFileTypes に含まれない MIME type は拒否される', async () => { + const createdRole = await assignRole(bob.id, { + uploadableFileTypes: { + useDefault: false, + priority: 1, + value: ['image/png'], + }, + }); + + try { + const res = await uploadFile(bob, { path: '192.jpg' }); + + assert.strictEqual(res.status, 400); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'UNALLOWED_FILE_TYPE'); + } finally { + await cleanupRole(bob.id, createdRole.id); + } + }); + + test('maxFileSizeMb 制限付きロールでも制限内ならアップロードできる', async () => { + const allowAllTypesRole = await assignRole(bob.id, { + uploadableFileTypes: { + useDefault: false, + priority: 1, + value: ['*/*'], + }, + }); + const tinyAttachmentRole = await assignRole(bob.id, { + maxFileSizeMb: { + useDefault: false, + priority: 1, + value: 10 / 1024 / 1024, // 10バイト + }, + }); + + try { + const res = await uploadFile(bob, { + blob: new Blob([new Uint8Array(10)]), + }); + + assert.strictEqual(res.status, 200); + } finally { + await cleanupRole(bob.id, tinyAttachmentRole.id); + await cleanupRole(bob.id, allowAllTypesRole.id); + } + }); + + test('maxFileSizeMb 制限を超えると 413 になる', async () => { + const allowAllTypesRole = await assignRole(bob.id, { + uploadableFileTypes: { + useDefault: false, + priority: 1, + value: ['*/*'], + }, + }); + const tinyAttachmentRole = await assignRole(bob.id, { + maxFileSizeMb: { + useDefault: false, + priority: 1, + value: 10 / 1024 / 1024, // 10バイト + }, + }); + + try { + const res = await uploadFile(bob, { + blob: new Blob([new Uint8Array(11)]), + }); + + assert.strictEqual(res.status, 413); + assert.ok(res.body); + assert.strictEqual(castAsError(res.body).error.code, 'MAX_FILE_SIZE_EXCEEDED'); + } finally { + await cleanupRole(bob.id, tinyAttachmentRole.id); + await cleanupRole(bob.id, allowAllTypesRole.id); + } + }); }); describe('drive/files/update', () => { diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts index 19433f3c88..1742506306 100644 --- a/packages/backend/test/e2e/exports.ts +++ b/packages/backend/test/e2e/exports.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; import { api, port, post, signup, startJobQueue } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/fetch-resource.ts b/packages/backend/test/e2e/fetch-resource.ts index f00843de10..f2d1c8e2e2 100644 --- a/packages/backend/test/e2e/fetch-resource.ts +++ b/packages/backend/test/e2e/fetch-resource.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, beforeEach, describe, test } from 'vitest'; import { api, channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js'; import type { SimpleGetResponse } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/fetch-validate-ap-deny.ts b/packages/backend/test/e2e/fetch-validate-ap-deny.ts index 434a9fe209..44e2925885 100644 --- a/packages/backend/test/e2e/fetch-validate-ap-deny.ts +++ b/packages/backend/test/e2e/fetch-validate-ap-deny.ts @@ -5,6 +5,7 @@ process.env.NODE_ENV = 'test'; +import { beforeAll, describe, test, expect } from 'vitest'; import { validateContentTypeSetAsActivityPub, validateContentTypeSetAsJsonLD } from '@/core/activitypub/misc/validator.js'; import { signup, uploadFile, relativeFetch } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/ff-visibility.ts b/packages/backend/test/e2e/ff-visibility.ts index 02582ae815..e0891d424d 100644 --- a/packages/backend/test/e2e/ff-visibility.ts +++ b/packages/backend/test/e2e/ff-visibility.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { api, signup, simpleGet } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index fd798bdb25..791b4f1d9b 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -9,6 +9,7 @@ process.env.NODE_ENV = 'test'; import { setTimeout } from 'node:timers/promises'; import * as assert from 'assert'; +import { afterAll, beforeAll, afterEach, describe, test } from 'vitest'; import { loadConfig } from '@/config.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; diff --git a/packages/backend/test/e2e/mute.ts b/packages/backend/test/e2e/mute.ts index b464c24287..f5cc875e7c 100644 --- a/packages/backend/test/e2e/mute.ts +++ b/packages/backend/test/e2e/mute.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { api, post, react, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts index 28b96fe8c8..8fe910ed78 100644 --- a/packages/backend/test/e2e/nodeinfo.ts +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, test } from 'vitest'; import { relativeFetch } from '../utils.js'; describe('nodeinfo', () => { diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 5937eb9b49..4e506a6202 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -8,6 +8,7 @@ import type { Repository } from "typeorm"; process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, afterAll, test } from 'vitest'; import { MiNote } from '@/models/Note.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index 67a9026eb5..82816f705e 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -11,6 +11,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { afterAll, beforeAll, beforeEach, describe, test } from 'vitest'; import { AuthorizationCode, type AuthorizationTokenConfig, diff --git a/packages/backend/test/e2e/renote-mute.ts b/packages/backend/test/e2e/renote-mute.ts index 0f636b9ae2..785c9dff8b 100644 --- a/packages/backend/test/e2e/renote-mute.ts +++ b/packages/backend/test/e2e/renote-mute.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { setTimeout } from 'node:timers/promises'; import { api, post, signup, waitFire } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/reversi-game.ts b/packages/backend/test/e2e/reversi-game.ts index 788255beac..7ff8d09cf3 100644 --- a/packages/backend/test/e2e/reversi-game.ts +++ b/packages/backend/test/e2e/reversi-game.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { ReversiMatchResponse } from 'misskey-js/entities.js'; import { api, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 72f26a38e0..18762ac383 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { WebSocket } from 'ws'; import { MiFollowing } from '@/models/Following.js'; import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js'; @@ -172,7 +173,7 @@ describe('Streaming', () => { const fired = await waitFire( ayano, 'homeTimeline', // ayano:home () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo', + msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.replyId === note.id, ); assert.strictEqual(fired, true); diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts index c98d199f35..a6ff6f1f5e 100644 --- a/packages/backend/test/e2e/synalio/abuse-report.ts +++ b/packages/backend/test/e2e/synalio/abuse-report.ts @@ -4,7 +4,14 @@ */ import { entities } from 'misskey-js'; -import { beforeEach, describe, test } from '@jest/globals'; +import { + beforeEach, + beforeAll, + afterAll, + describe, + expect, + test, +} from 'vitest'; import { api, captureWebhook, diff --git a/packages/backend/test/e2e/synalio/user-create.ts b/packages/backend/test/e2e/synalio/user-create.ts index cb0f68dfea..8910954cd6 100644 --- a/packages/backend/test/e2e/synalio/user-create.ts +++ b/packages/backend/test/e2e/synalio/user-create.ts @@ -5,7 +5,7 @@ import { setTimeout } from 'node:timers/promises'; import { entities } from 'misskey-js'; -import { beforeEach, describe, test } from '@jest/globals'; +import { beforeEach, describe, test, beforeAll, afterAll, expect } from 'vitest'; import { api, captureWebhook, diff --git a/packages/backend/test/e2e/thread-mute.ts b/packages/backend/test/e2e/thread-mute.ts index 1edc178fc2..e772379125 100644 --- a/packages/backend/test/e2e/thread-mute.ts +++ b/packages/backend/test/e2e/thread-mute.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { api, connectStream, post, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 4fd826100d..8a23657772 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -9,6 +9,7 @@ // pnpm jest -- e2e/timelines.ts import * as assert from 'assert'; +import { describe, beforeAll, test } from 'vitest'; import { setTimeout } from 'node:timers/promises'; import { entities } from 'misskey-js'; import { Redis } from 'ioredis'; diff --git a/packages/backend/test/e2e/user-notes.ts b/packages/backend/test/e2e/user-notes.ts index cc07c5ae71..2f89ac54ce 100644 --- a/packages/backend/test/e2e/user-notes.ts +++ b/packages/backend/test/e2e/user-notes.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { api, post, signup, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index a342bba64c..be5fb3b0a7 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, beforeEach, describe, test } from 'vitest'; import { inspect } from 'node:util'; import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts index 538a990a4e..51774c68a6 100644 --- a/packages/backend/test/e2e/well-known.ts +++ b/packages/backend/test/e2e/well-known.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { api, host, origin, relativeFetch, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; diff --git a/packages/backend/test/eslint.config.js b/packages/backend/test/eslint.config.js index a0f43babad..e3bcf4c0fe 100644 --- a/packages/backend/test/eslint.config.js +++ b/packages/backend/test/eslint.config.js @@ -9,7 +9,6 @@ export default [ languageOptions: { globals: { ...globals.node, - ...globals.jest, }, parserOptions: { parser: tsParser, diff --git a/packages/backend/test/prelude/url.ts b/packages/backend/test/prelude/url.ts index b26ae09444..a2412288b4 100644 --- a/packages/backend/test/prelude/url.ts +++ b/packages/backend/test/prelude/url.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { describe, test } from 'vitest'; import { query } from '../../src/misc/prelude/url.js'; describe('url', () => { diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/setup.e2e.ts similarity index 67% rename from packages/backend/test/jest.setup.ts rename to packages/backend/test/setup.e2e.ts index 9185f58acb..3141dc15ad 100644 --- a/packages/backend/test/jest.setup.ts +++ b/packages/backend/test/setup.e2e.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { beforeAll } from 'vitest'; import { initTestDb, sendEnvResetRequest } from './utils.js'; beforeAll(async () => { - await initTestDb(false); - await sendEnvResetRequest(); + await initTestDb(false); + await sendEnvResetRequest(); }); - diff --git a/packages/backend/test/jest.setup.unit.cjs b/packages/backend/test/setup.unit.ts similarity index 86% rename from packages/backend/test/jest.setup.unit.cjs rename to packages/backend/test/setup.unit.ts index dd879c81c8..e9adab9d64 100644 --- a/packages/backend/test/jest.setup.unit.cjs +++ b/packages/backend/test/setup.unit.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -module.exports = async () => { +export default function setup() { // DBはUTC(っぽい)ので、テスト側も合わせておく process.env.TZ = 'UTC'; process.env.NODE_ENV = 'test'; -}; +} diff --git a/packages/backend/test/tsconfig.json b/packages/backend/test/tsconfig.json index a2a86c696e..b857cf927a 100644 --- a/packages/backend/test/tsconfig.json +++ b/packages/backend/test/tsconfig.json @@ -3,7 +3,7 @@ "allowJs": true, "noEmitOnError": false, "noImplicitAny": true, - "noImplicitReturns": true, + "noImplicitReturns": false, "noUnusedParameters": false, "noUnusedLocals": false, "noFallthroughCasesInSwitch": true, @@ -35,7 +35,7 @@ "lib": [ "esnext" ], - "types": ["jest", "node"] + "types": ["node"] }, "compileOnSave": false, "include": [ diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts index 9dad8e229d..f4d8e9098e 100644 --- a/packages/backend/test/unit/AbuseReportNotificationService.ts +++ b/packages/backend/test/unit/AbuseReportNotificationService.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, jest } from '@jest/globals'; +import { describe, expect, beforeAll, afterAll, beforeEach, afterEach, test, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { randomString } from '../utils.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; @@ -42,9 +43,9 @@ describe('AbuseReportNotificationService', () => { let systemWebhooksRepository: SystemWebhooksRepository; let abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository; let idService: IdService; - let roleService: jest.Mocked; - let emailService: jest.Mocked; - let webhookService: jest.Mocked; + let roleService: Mocked; + let emailService: Mocked; + let webhookService: Mocked; // -------------------------------------------------------------------------------------- @@ -107,10 +108,10 @@ describe('AbuseReportNotificationService', () => { AbuseReportNotificationService, IdService, { - provide: RoleService, useFactory: () => ({ getModeratorIds: jest.fn() }), + provide: RoleService, useFactory: () => ({ getModeratorIds: vi.fn() }), }, { - provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), + provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: vi.fn() }), }, { provide: UserEntityService, useFactory: () => ({ @@ -119,16 +120,16 @@ describe('AbuseReportNotificationService', () => { }), }, { - provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), + provide: EmailService, useFactory: () => ({ sendEmail: vi.fn() }), }, { - provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), + provide: MetaService, useFactory: () => ({ fetch: vi.fn() }), }, { provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), }, { - provide: GlobalEventService, useFactory: () => ({ publishAdminStream: jest.fn() }), + provide: GlobalEventService, useFactory: () => ({ publishAdminStream: vi.fn() }), }, ], }) @@ -141,9 +142,9 @@ describe('AbuseReportNotificationService', () => { service = app.get(AbuseReportNotificationService); idService = app.get(IdService); - roleService = app.get(RoleService) as jest.Mocked; - emailService = app.get(EmailService) as jest.Mocked; - webhookService = app.get(SystemWebhookService) as jest.Mocked; + roleService = app.get(RoleService) as Mocked; + emailService = app.get(EmailService) as Mocked; + webhookService = app.get(SystemWebhookService) as Mocked; app.enableShutdownHooks(); }); diff --git a/packages/backend/test/unit/AnnouncementService.ts b/packages/backend/test/unit/AnnouncementService.ts index b3f7f426fe..ea0a1abc17 100644 --- a/packages/backend/test/unit/AnnouncementService.ts +++ b/packages/backend/test/unit/AnnouncementService.ts @@ -5,8 +5,9 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; -import { ModuleMocker } from 'jest-mock'; +import { describe, expect, beforeEach, afterEach, test, vi } from 'vitest'; +import type { Mocked } from 'vitest'; +import { mockDeep } from 'vitest-mock-extended'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { AnnouncementService } from '@/core/AnnouncementService.js'; @@ -26,9 +27,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import type { TestingModule } from '@nestjs/testing'; -import type { MockMetadata } from 'jest-mock'; - -const moduleMocker = new ModuleMocker(global); describe('AnnouncementService', () => { let app: TestingModule; @@ -36,8 +34,8 @@ describe('AnnouncementService', () => { let usersRepository: UsersRepository; let announcementsRepository: AnnouncementsRepository; let announcementReadsRepository: AnnouncementReadsRepository; - let globalEventService: jest.Mocked; - let moderationLogService: jest.Mocked; + let globalEventService: Mocked; + let moderationLogService: Mocked; function createUser(data: Partial = {}) { const un = secureRndstr(16); @@ -76,17 +74,15 @@ describe('AnnouncementService', () => { .useMocker((token) => { if (token === GlobalEventService) { return { - publishMainStream: jest.fn(), - publishBroadcastStream: jest.fn(), + publishMainStream: vi.fn(), + publishBroadcastStream: vi.fn(), }; } else if (token === ModerationLogService) { return { - log: jest.fn(), + log: vi.fn(), }; } else if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); + return mockDeep(); } }) .compile(); @@ -97,8 +93,8 @@ describe('AnnouncementService', () => { usersRepository = app.get(DI.usersRepository); announcementsRepository = app.get(DI.announcementsRepository); announcementReadsRepository = app.get(DI.announcementReadsRepository); - globalEventService = app.get(GlobalEventService) as jest.Mocked; - moderationLogService = app.get(ModerationLogService) as jest.Mocked; + globalEventService = app.get(GlobalEventService) as Mocked; + moderationLogService = app.get(ModerationLogService) as Mocked; }); afterEach(async () => { @@ -203,7 +199,7 @@ describe('AnnouncementService', () => { }); }); - describe('read', () => { + describe.todo('read', () => { // TODO }); }); diff --git a/packages/backend/test/unit/ApMfmService.ts b/packages/backend/test/unit/ApMfmService.ts index 93efa5d7d3..c020ca98c0 100644 --- a/packages/backend/test/unit/ApMfmService.ts +++ b/packages/backend/test/unit/ApMfmService.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { describe, test, beforeAll } from 'vitest'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts index 24bb81118e..5a013d5cd2 100644 --- a/packages/backend/test/unit/CaptchaService.ts +++ b/packages/backend/test/unit/CaptchaService.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals'; +import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { Response } from 'node-fetch'; import { @@ -22,8 +23,8 @@ import { LoggerService } from '@/core/LoggerService.js'; describe('CaptchaService', () => { let app: TestingModule; let service: CaptchaService; - let httpRequestService: jest.Mocked; - let metaService: jest.Mocked; + let httpRequestService: Mocked; + let metaService: Mocked; beforeAll(async () => { app = await Test.createTestingModule({ @@ -34,12 +35,12 @@ describe('CaptchaService', () => { CaptchaService, LoggerService, { - provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }), + provide: HttpRequestService, useFactory: () => ({ send: vi.fn() }), }, { provide: MetaService, useFactory: () => ({ - fetch: jest.fn(), - update: jest.fn(), + fetch: vi.fn(), + update: vi.fn(), }), }, ], @@ -48,8 +49,8 @@ describe('CaptchaService', () => { app.enableShutdownHooks(); service = app.get(CaptchaService); - httpRequestService = app.get(HttpRequestService) as jest.Mocked; - metaService = app.get(MetaService) as jest.Mocked; + httpRequestService = app.get(HttpRequestService) as Mocked; + metaService = app.get(MetaService) as Mocked; }); beforeEach(() => { diff --git a/packages/backend/test/unit/ChannelFollowingService.ts b/packages/backend/test/unit/ChannelFollowingService.ts index 2d3196f2f4..3b1ad72287 100644 --- a/packages/backend/test/unit/ChannelFollowingService.ts +++ b/packages/backend/test/unit/ChannelFollowingService.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { afterEach, beforeEach, describe, expect } from '@jest/globals'; +import { afterEach, beforeEach, describe, expect, beforeAll, afterAll, test } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/ChannelMutingService.ts b/packages/backend/test/unit/ChannelMutingService.ts index 6916701d1f..0c59f141a4 100644 --- a/packages/backend/test/unit/ChannelMutingService.ts +++ b/packages/backend/test/unit/ChannelMutingService.ts @@ -5,7 +5,7 @@ /* eslint-disable */ -import { afterEach, beforeEach, describe, expect } from '@jest/globals'; +import { afterEach, beforeEach, describe, expect, beforeAll, afterAll, test } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/CustomEmojiService.ts b/packages/backend/test/unit/CustomEmojiService.ts index d6c73a2091..e8cad09e9b 100644 --- a/packages/backend/test/unit/CustomEmojiService.ts +++ b/packages/backend/test/unit/CustomEmojiService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { afterEach, beforeAll, describe, test } from '@jest/globals'; +import { afterEach, beforeAll, describe, test, expect } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; diff --git a/packages/backend/test/unit/DriveService.ts b/packages/backend/test/unit/DriveService.ts index 48b108fbba..eafc49973b 100644 --- a/packages/backend/test/unit/DriveService.ts +++ b/packages/backend/test/unit/DriveService.ts @@ -5,6 +5,7 @@ process.env.NODE_ENV = 'test'; +import { afterAll, beforeAll, beforeEach, describe, test, expect } from 'vitest'; import { Test } from '@nestjs/testing'; import { DeleteObjectCommand, diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 1e3605aafc..d2c2dcf283 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -5,7 +5,8 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test } from '@nestjs/testing'; import { Redis } from 'ioredis'; import type { TestingModule } from '@nestjs/testing'; @@ -18,22 +19,32 @@ import { UtilityService } from '@/core/UtilityService.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; -function mockRedis() { - const hash = {} as any; - const set = jest.fn((key: string, value) => { - const ret = hash[key]; - hash[key] = value; - return ret; +function createMockRedis() { + const store = new Map(); + + const del = vi.fn((key: string) => { + const existed = store.delete(key); + return Promise.resolve(existed ? 1 : 0); }); - return set; + + const set = vi.fn((key: string, value: string, ...args: any[]) => { + const prev = store.get(key) ?? null; + store.set(key, value); + + // ioredis: SET key value ... GET => returns old value or null + const hasGet = args.some(a => typeof a === 'string' && a.toUpperCase() === 'GET'); + return Promise.resolve(hasGet ? prev : 'OK'); + }); + + return { set, del }; } describe('FetchInstanceMetadataService', () => { let app: TestingModule; - let fetchInstanceMetadataService: jest.Mocked; - let federatedInstanceService: jest.Mocked; - let httpRequestService: jest.Mocked; - let redisClient: jest.Mocked; + let fetchInstanceMetadataService: Mocked; + let federatedInstanceService: Mocked; + let httpRequestService: Mocked; + let redisClient: Mocked; beforeEach(async () => { app = await Test @@ -50,11 +61,11 @@ describe('FetchInstanceMetadataService', () => { }) .useMocker((token) => { if (token === HttpRequestService) { - return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() }; + return { getJson: vi.fn(), getHtml: vi.fn(), send: vi.fn() }; } else if (token === FederatedInstanceService) { - return { fetchOrRegister: jest.fn() }; + return { fetchOrRegister: vi.fn() }; } else if (token === DI.redis) { - return mockRedis; + return createMockRedis(); } return null; }) @@ -62,23 +73,24 @@ describe('FetchInstanceMetadataService', () => { app.enableShutdownHooks(); - fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as jest.Mocked; - federatedInstanceService = app.get(FederatedInstanceService) as jest.Mocked; - redisClient = app.get(DI.redis) as jest.Mocked; - httpRequestService = app.get(HttpRequestService) as jest.Mocked; + fetchInstanceMetadataService = app.get(FetchInstanceMetadataService) as Mocked; + federatedInstanceService = app.get(FederatedInstanceService) as Mocked; + redisClient = app.get(DI.redis) as Mocked; + httpRequestService = app.get(HttpRequestService) as Mocked; }); afterEach(async () => { await app.close(); + vi.resetAllMocks(); + vi.clearAllMocks(); }); test('Lock and update', async () => { - redisClient.set = mockRedis(); const now = Date.now(); federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); expect(tryLockSpy).toHaveBeenCalledTimes(1); @@ -88,12 +100,11 @@ describe('FetchInstanceMetadataService', () => { }); test('Lock and don\'t update', async () => { - redisClient.set = mockRedis(); const now = Date.now(); federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); expect(tryLockSpy).toHaveBeenCalledTimes(1); @@ -103,13 +114,12 @@ describe('FetchInstanceMetadataService', () => { }); test('Do nothing when lock not acquired', async () => { - redisClient.set = mockRedis(); const now = Date.now(); federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); await fetchInstanceMetadataService.tryLock('example.com'); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); expect(tryLockSpy).toHaveBeenCalledTimes(1); @@ -119,13 +129,12 @@ describe('FetchInstanceMetadataService', () => { }); test('Do when lock not acquired but forced', async () => { - redisClient.set = mockRedis(); const now = Date.now(); federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); await fetchInstanceMetadataService.tryLock('example.com'); - const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); - const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + const tryLockSpy = vi.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = vi.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true); expect(tryLockSpy).toHaveBeenCalledTimes(0); diff --git a/packages/backend/test/unit/FileInfoService.ts b/packages/backend/test/unit/FileInfoService.ts index 28a2a971f4..fa4a13b171 100644 --- a/packages/backend/test/unit/FileInfoService.ts +++ b/packages/backend/test/unit/FileInfoService.ts @@ -8,23 +8,20 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; -import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; -import { afterAll, beforeAll, describe, test } from '@jest/globals'; +import { afterAll, beforeAll, describe, test } from 'vitest'; +import { mockDeep } from 'vitest-mock-extended'; import { GlobalModule } from '@/GlobalModule.js'; import { FileInfo, FileInfoService } from '@/core/FileInfoService.js'; //import { DI } from '@/di-symbols.js'; import { AiService } from '@/core/AiService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { TestingModule } from '@nestjs/testing'; -import type { MockMetadata } from 'jest-mock'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const resources = `${_dirname}/../resources`; -const moduleMocker = new ModuleMocker(global); - describe('FileInfoService', () => { let app: TestingModule; let fileInfoService: FileInfoService; @@ -54,9 +51,7 @@ describe('FileInfoService', () => { // return { }; //} if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); + return mockDeep(); } }) .compile(); diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts index 91c2286ff6..a5f7a78195 100644 --- a/packages/backend/test/unit/FlashService.ts +++ b/packages/backend/test/unit/FlashService.ts @@ -5,6 +5,7 @@ */ import { Test, TestingModule } from '@nestjs/testing'; +import { afterAll, afterEach, beforeEach, describe, expect, test } from 'vitest'; import { FlashService } from '@/core/FlashService.js'; import { IdService } from '@/core/IdService.js'; import { FlashLikesRepository, FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; diff --git a/packages/backend/test/unit/MetaService.ts b/packages/backend/test/unit/MetaService.ts index 19c98eab3d..4396e26f79 100644 --- a/packages/backend/test/unit/MetaService.ts +++ b/packages/backend/test/unit/MetaService.ts @@ -5,7 +5,7 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; +import { afterAll, beforeAll, describe, test, expect, vi } from 'vitest'; import { Test } from '@nestjs/testing'; import { GlobalModule } from '@/GlobalModule.js'; import { DI } from '@/di-symbols.js'; @@ -40,7 +40,7 @@ describe('MetaService', () => { test('fetch (cache)', async () => { const db = app.get(DI.db); - const spy = jest.spyOn(db, 'transaction'); + const spy = vi.spyOn(db, 'transaction'); const result = await metaService.fetch(); @@ -50,7 +50,7 @@ describe('MetaService', () => { test('fetch (force)', async () => { const db = app.get(DI.db); - const spy = jest.spyOn(db, 'transaction'); + const spy = vi.spyOn(db, 'transaction'); const result = await metaService.fetch(true); diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 2f5f3745de..358209b4d7 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import * as mfm from 'mfm-js'; +import { beforeAll, describe, test } from 'vitest'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index f3d3d1da99..3e493fd3d9 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { beforeAll, describe, test, expect } from 'vitest'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/ReactionService.ts b/packages/backend/test/unit/ReactionService.ts index 3cfb4ff3f8..8093e3a3ec 100644 --- a/packages/backend/test/unit/ReactionService.ts +++ b/packages/backend/test/unit/ReactionService.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { beforeAll, describe, test } from 'vitest'; import { Test } from '@nestjs/testing'; import { CoreModule } from '@/core/CoreModule.js'; diff --git a/packages/backend/test/unit/RelayService.ts b/packages/backend/test/unit/RelayService.ts index bee580d0c7..0d3ce83b11 100644 --- a/packages/backend/test/unit/RelayService.ts +++ b/packages/backend/test/unit/RelayService.ts @@ -5,11 +5,11 @@ process.env.NODE_ENV = 'test'; -import { jest } from '@jest/globals'; +import { afterAll, beforeAll, describe, test, expect, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test } from '@nestjs/testing'; -import { ModuleMocker } from 'jest-mock'; +import { mockDeep } from 'vitest-mock-extended'; import type { TestingModule } from '@nestjs/testing'; -import type { MockMetadata } from 'jest-mock'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { IdService } from '@/core/IdService.js'; @@ -19,12 +19,10 @@ import { SystemAccountService } from '@/core/SystemAccountService.js'; import { GlobalModule } from '@/GlobalModule.js'; import { UtilityService } from '@/core/UtilityService.js'; -const moduleMocker = new ModuleMocker(global); - describe('RelayService', () => { let app: TestingModule; let relayService: RelayService; - let queueService: jest.Mocked; + let queueService: Mocked; beforeAll(async () => { app = await Test.createTestingModule({ @@ -42,12 +40,10 @@ describe('RelayService', () => { }) .useMocker((token) => { if (token === QueueService) { - return { deliver: jest.fn() }; + return { deliver: vi.fn() }; } if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); + return mockDeep(); } }) .compile(); @@ -55,7 +51,7 @@ describe('RelayService', () => { app.enableShutdownHooks(); relayService = app.get(RelayService); - queueService = app.get(QueueService) as jest.Mocked; + queueService = app.get(QueueService) as Mocked; }); afterAll(async () => { diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 9b17b1fbb9..ec1e7ca134 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -6,12 +6,12 @@ process.env.NODE_ENV = 'test'; import { setTimeout } from 'node:timers/promises'; -import { describe, jest } from '@jest/globals'; -import { ModuleMocker } from 'jest-mock'; +import { describe, beforeEach, afterEach, test, expect, vi } from 'vitest'; +import type { Mocked } from 'vitest'; +import { mockDeep } from 'vitest-mock-extended'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import type { TestingModule } from '@nestjs/testing'; -import type { MockMetadata } from 'jest-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import { @@ -34,17 +34,15 @@ import { NotificationService } from '@/core/NotificationService.js'; import { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -const moduleMocker = new ModuleMocker(global); - describe('RoleService', () => { let app: TestingModule; let roleService: RoleService; let usersRepository: UsersRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; - let meta: jest.Mocked; - let notificationService: jest.Mocked; - let clock: lolex.InstalledClock; + let meta: Mocked; + let notificationService: Mocked; + let clock: lolex.Clock; async function createUser(data: Partial = {}) { const un = secureRndstr(16); @@ -123,7 +121,7 @@ describe('RoleService', () => { { provide: NotificationService, useFactory: () => ({ - createNotification: jest.fn(), + createNotification: vi.fn(), }), }, { @@ -134,12 +132,10 @@ describe('RoleService', () => { }) .useMocker((token) => { if (token === MetaService) { - return { fetch: jest.fn() }; + return { fetch: vi.fn() }; } if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); + return mockDeep(); } }) .compile(); @@ -151,8 +147,8 @@ describe('RoleService', () => { rolesRepository = app.get(DI.rolesRepository); roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); - meta = app.get(DI.meta) as jest.Mocked; - notificationService = app.get(NotificationService) as jest.Mocked; + meta = app.get(DI.meta) as Mocked; + notificationService = app.get(NotificationService) as Mocked; await roleService.onModuleInit(); }); @@ -163,7 +159,7 @@ describe('RoleService', () => { /** * Delete meta and roleAssignment first to avoid deadlock due to schema dependencies * https://github.com/misskey-dev/misskey/issues/16783 - */ + */ await app.get(DI.metasRepository).createQueryBuilder().delete().execute(); await roleAssignmentsRepository.createQueryBuilder().delete().execute(); await Promise.all([ @@ -700,6 +696,19 @@ describe('RoleService', () => { expect(adminIds).toHaveLength(0); }); + test('should not include duplicate user IDs if a user has multiple administrator roles', async () => { + const adminUser = await createUser(); + const adminRole1 = await createRole({ name: 'admin1', isAdministrator: true }); + const adminRole2 = await createRole({ name: 'admin2', isAdministrator: true }); + + await roleService.assign(adminUser.id, adminRole1.id); + await roleService.assign(adminUser.id, adminRole2.id); + + const adminIds = await roleService.getAdministratorIds(); + + expect(adminIds).toEqual([adminUser.id]); + }); + // TODO: rootユーザーは現在実装に含まれていないため、テストもそれに倣う test('should not include the root user', async () => { const rootUser = await createUser(); diff --git a/packages/backend/test/unit/S3Service.ts b/packages/backend/test/unit/S3Service.ts index 6e7e5a8b59..dce1e2065d 100644 --- a/packages/backend/test/unit/S3Service.ts +++ b/packages/backend/test/unit/S3Service.ts @@ -5,6 +5,7 @@ process.env.NODE_ENV = 'test'; +import { afterAll, beforeAll, beforeEach, describe, test, expect } from 'vitest'; import { Test } from '@nestjs/testing'; import { CompleteMultipartUploadCommand, diff --git a/packages/backend/test/unit/SearchService.ts b/packages/backend/test/unit/SearchService.ts index 6e17bef1c3..867d1a424f 100644 --- a/packages/backend/test/unit/SearchService.ts +++ b/packages/backend/test/unit/SearchService.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { afterAll, afterEach, beforeAll, describe, expect, test } from '@jest/globals'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; -import type { Index, MeiliSearch } from 'meilisearch'; +import type { Index, Meilisearch } from 'meilisearch'; import { type Config, loadConfig } from '@/config.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; @@ -416,7 +416,7 @@ describe('SearchService', () => { describe('meilisearch', () => { let ctx: TestContext; - let meilisearch: MeiliSearch; + let meilisearch: Meilisearch; let meilisearchIndex: Index; let meiliConfig: Config; @@ -438,7 +438,7 @@ describe('SearchService', () => { }; ctx = await buildContext(meiliConfig); - meilisearch = ctx.app.get(DI.meilisearch) as MeiliSearch; + meilisearch = ctx.app.get(DI.meilisearch) as Meilisearch; meilisearchIndex = meilisearch.index(`${meiliConfig.meilisearch!.index}---notes`); const settingsTask = await meilisearchIndex.updateSettings(meilisearchSettings); diff --git a/packages/backend/test/unit/SigninWithPasskeyApiService.ts b/packages/backend/test/unit/SigninWithPasskeyApiService.ts index 8ef46024ac..f989d2d29a 100644 --- a/packages/backend/test/unit/SigninWithPasskeyApiService.ts +++ b/packages/backend/test/unit/SigninWithPasskeyApiService.ts @@ -4,12 +4,12 @@ */ import { IncomingHttpHeaders } from 'node:http'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { mockDeep } from 'vitest-mock-extended'; import { Test, TestingModule } from '@nestjs/testing'; import { FastifyReply, FastifyRequest } from 'fastify'; import { AuthenticationResponseJSON } from '@simplewebauthn/types'; import { HttpHeader } from 'fastify/types/utils.js'; -import { MockMetadata, ModuleMocker } from 'jest-mock'; import { MiUser } from '@/models/User.js'; import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; @@ -22,8 +22,6 @@ import { WebAuthnService } from '@/core/WebAuthnService.js'; import { SigninService } from '@/server/api/SigninService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -const moduleMocker = new ModuleMocker(global); - class FakeLimiter { public async limit() { return; @@ -95,9 +93,7 @@ describe('SigninWithPasskeyApiService', () => { ], }).useMocker((token) => { if (typeof token === 'function') { - const mockMetadata = moduleMocker.getMetadata(token) as MockMetadata; - const Mock = moduleMocker.generateFromMetadata(mockMetadata); - return new Mock(); + return mockDeep(); } }).compile(); passkeyApiService = app.get(SigninWithPasskeyApiService); @@ -112,7 +108,7 @@ describe('SigninWithPasskeyApiService', () => { FakeWebauthnVerify = async () => { return uid; }; - jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); + vi.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify); const dummyUser = { id: uid, username: uid, usernameLower: uid.toLowerCase(), uri: null, host: null, @@ -159,7 +155,7 @@ describe('SigninWithPasskeyApiService', () => { it('Should return 403 When Challenge Verify fail', async () => { const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType; const res = new DummyFastifyReply() as FastifyReply; - jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') + vi.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication') .mockImplementation(async () => { throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED'); }); diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts index 1128d83be1..a6534a5b1b 100644 --- a/packages/backend/test/unit/SystemWebhookService.ts +++ b/packages/backend/test/unit/SystemWebhookService.ts @@ -5,7 +5,8 @@ */ import { setTimeout } from 'node:timers/promises'; -import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; +import { afterEach, beforeEach, afterAll, beforeAll, describe, test, expect, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { randomString } from '../utils.js'; import { MiUser } from '@/models/User.js'; @@ -29,7 +30,7 @@ describe('SystemWebhookService', () => { let usersRepository: UsersRepository; let systemWebhooksRepository: SystemWebhooksRepository; let idService: IdService; - let queueService: jest.Mocked; + let queueService: Mocked; // -------------------------------------------------------------------------------------- @@ -73,7 +74,7 @@ describe('SystemWebhookService', () => { LoggerService, GlobalEventService, { - provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }), + provide: QueueService, useFactory: () => ({ systemWebhookDeliver: vi.fn() }), }, { provide: ModerationLogService, useFactory: () => ({ log: () => Promise.resolve() }), @@ -87,7 +88,7 @@ describe('SystemWebhookService', () => { service = app.get(SystemWebhookService); idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; + queueService = app.get(QueueService) as Mocked; app.enableShutdownHooks(); } diff --git a/packages/backend/test/unit/UserSearchService.ts b/packages/backend/test/unit/UserSearchService.ts index 75d3e58adc..28fcb0cb5a 100644 --- a/packages/backend/test/unit/UserSearchService.ts +++ b/packages/backend/test/unit/UserSearchService.ts @@ -4,7 +4,7 @@ */ import { Test, TestingModule } from '@nestjs/testing'; -import { describe, jest, test } from '@jest/globals'; +import { describe, beforeEach, beforeAll, afterEach, afterAll, vi, test, expect } from 'vitest'; import { In } from 'typeorm'; import { UserSearchService } from '@/core/UserSearchService.js'; import { FollowingsRepository, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; @@ -92,7 +92,7 @@ describe('UserSearchService', () => { providers: [ UserSearchService, { - provide: UserEntityService, useFactory: jest.fn(() => ({ + provide: UserEntityService, useFactory: vi.fn(() => ({ // とりあえずIDが返れば確認が出来るので packMany: (value: any) => value, })), diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts index 928b9d3c2b..5250b66cc6 100644 --- a/packages/backend/test/unit/UserWebhookService.ts +++ b/packages/backend/test/unit/UserWebhookService.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; +import { afterEach, beforeEach, describe, expect, test, beforeAll, afterAll, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { randomString } from '../utils.js'; import { MiUser } from '@/models/User.js'; @@ -25,7 +26,7 @@ describe('UserWebhookService', () => { let usersRepository: UsersRepository; let userWebhooksRepository: WebhooksRepository; let idService: IdService; - let queueService: jest.Mocked; + let queueService: Mocked; // -------------------------------------------------------------------------------------- @@ -70,7 +71,7 @@ describe('UserWebhookService', () => { LoggerService, GlobalEventService, { - provide: QueueService, useFactory: () => ({ userWebhookDeliver: jest.fn() }), + provide: QueueService, useFactory: () => ({ userWebhookDeliver: vi.fn() }), }, ], }) @@ -81,7 +82,7 @@ describe('UserWebhookService', () => { service = app.get(UserWebhookService); idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; + queueService = app.get(QueueService) as Mocked; app.enableShutdownHooks(); } diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts index 0e965021c2..91a17719d2 100644 --- a/packages/backend/test/unit/WebhookTestService.ts +++ b/packages/backend/test/unit/WebhookTestService.ts @@ -5,7 +5,8 @@ */ import { Test, TestingModule } from '@nestjs/testing'; -import { beforeAll, describe, jest } from '@jest/globals'; +import { beforeAll, afterAll, beforeEach, afterEach, test, expect, describe, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { WebhookTestService } from '@/core/WebhookTestService.js'; import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; @@ -24,9 +25,9 @@ describe('WebhookTestService', () => { let usersRepository: UsersRepository; let userProfilesRepository: UserProfilesRepository; - let queueService: jest.Mocked; - let userWebhookService: jest.Mocked; - let systemWebhookService: jest.Mocked; + let queueService: Mocked; + let userWebhookService: Mocked; + let systemWebhookService: Mocked; let idService: IdService; let root: MiUser; @@ -59,23 +60,23 @@ describe('WebhookTestService', () => { IdService, { provide: CustomEmojiService, useFactory: () => ({ - populateEmojis: jest.fn(), + populateEmojis: vi.fn(), }), }, { provide: QueueService, useFactory: () => ({ - systemWebhookDeliver: jest.fn(), - userWebhookDeliver: jest.fn(), + systemWebhookDeliver: vi.fn(), + userWebhookDeliver: vi.fn(), }), }, { provide: UserWebhookService, useFactory: () => ({ - fetchWebhooks: jest.fn(), + fetchWebhooks: vi.fn(), }), }, { provide: SystemWebhookService, useFactory: () => ({ - fetchSystemWebhooks: jest.fn(), + fetchSystemWebhooks: vi.fn(), }), }, ], @@ -86,9 +87,9 @@ describe('WebhookTestService', () => { service = app.get(WebhookTestService); idService = app.get(IdService); - queueService = app.get(QueueService) as jest.Mocked; - userWebhookService = app.get(UserWebhookService) as jest.Mocked; - systemWebhookService = app.get(SystemWebhookService) as jest.Mocked; + queueService = app.get(QueueService) as Mocked; + userWebhookService = app.get(UserWebhookService) as Mocked; + systemWebhookService = app.get(SystemWebhookService) as Mocked; app.enableShutdownHooks(); }); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index c6e09bdda2..1ad61001f0 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -9,8 +9,8 @@ import * as assert from 'assert'; import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; +import { describe, beforeAll, beforeEach, test, vi } from 'vitest'; import { Test } from '@nestjs/testing'; -import { jest } from '@jest/globals'; import { MockResolver } from '../misc/mock-resolver.js'; import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; @@ -155,7 +155,7 @@ describe('ActivityPub', () => { // Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error const federatedInstanceService = app.get(FederatedInstanceService); - jest.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); + vi.spyOn(federatedInstanceService, 'fetch').mockImplementation(() => new Promise(() => { })); }); beforeEach(() => { @@ -224,6 +224,51 @@ describe('ActivityPub', () => { }); }); + describe('alsoKnownAs field', () => { + test('Handle alsoKnownAs as an array', async () => { + const actor = { + ...createRandomActor(), + alsoKnownAs: ['https://example.com/users/alice', 'https://example.com/users/alice2'], + }; + + resolver.register(actor.id, actor); + + const user = await personService.createPerson(actor.id, resolver); + + assert.deepStrictEqual(user.alsoKnownAs, actor.alsoKnownAs); + }); + + test('Handle alsoKnownAs as a string', async () => { + const actor = { + ...createRandomActor(), + alsoKnownAs: 'https://example.com/users/alice', + }; + + resolver.register(actor.id, actor); + + const user = await personService.createPerson(actor.id, resolver); + + assert.deepStrictEqual(user.alsoKnownAs, [actor.alsoKnownAs]); + }); + + test('Update person with alsoKnownAs as a string', async () => { + const actor = createRandomActor(); + resolver.register(actor.id, actor); + const user = await personService.createPerson(actor.id, resolver); + + const updatedActor = { + ...actor, + alsoKnownAs: 'https://example.com/users/alice', + }; + resolver.register(actor.id, updatedActor); + + await personService.updatePerson(actor.id, resolver, updatedActor); + + const updatedUser = await personService.fetchPerson(actor.id); + assert.deepStrictEqual(updatedUser?.alsoKnownAs, [updatedActor.alsoKnownAs]); + }); + }); + describe('Collection visibility', () => { test('Public following/followers', async () => { const actor = createRandomActor(); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index f8b2a697f2..83bcdb717b 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { describe, test } from 'vitest'; import * as assert from 'assert'; import httpSignature from '@peertube/http-signature'; @@ -78,7 +79,7 @@ describe('ap-request', () => { 'https://alice.example.com/abc', FetchAllowSoftFailMask.Any, ), 'validation should fail no matter what if the response URL is inconsistent with the object ID'); - + assert.doesNotThrow(() => assertActivityMatchesUrl( 'https://alice.example.com/abc#test', { id: 'https://alice.example.com/abc' } as IObject, diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 364a2c2fbd..7c10e4de72 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -6,7 +6,8 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { jest } from '@jest/globals'; +import { describe, beforeEach, afterEach, afterAll, test } from 'vitest'; +import type { Mocked } from 'vitest'; import * as lolex from '@sinonjs/fake-timers'; import { DataSource } from 'typeorm'; import * as Redis from 'ioredis'; @@ -28,13 +29,13 @@ describe('Chart', () => { let redisClient = { set: () => Promise.resolve('OK'), get: () => Promise.resolve(null), - } as unknown as jest.Mocked; + } as unknown as Mocked; let testChart: TestChart; let testGroupedChart: TestGroupedChart; let testUniqueChart: TestUniqueChart; let testIntersectionChart: TestIntersectionChart; - let clock: lolex.InstalledClock; + let clock: lolex.Clock; beforeEach(async () => { if (db) db.destroy(); diff --git a/packages/backend/test/unit/entities/DriveFileEntityService.ts b/packages/backend/test/unit/entities/DriveFileEntityService.ts index 2e416326ee..8fca5ffe12 100644 --- a/packages/backend/test/unit/entities/DriveFileEntityService.ts +++ b/packages/backend/test/unit/entities/DriveFileEntityService.ts @@ -5,7 +5,7 @@ process.env.NODE_ENV = 'test'; -import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals'; +import { afterAll, beforeAll, beforeEach, describe, expect, vi, test } from 'vitest'; import { Test } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing'; import type { DriveFilesRepository, DriveFoldersRepository, UsersRepository } from '@/models/_.js'; @@ -30,13 +30,13 @@ describe('DriveFileEntityService', () => { let idCounter = 0; const userEntityServiceMock = { - packMany: jest.fn(async (users: Array) => { + packMany: vi.fn(async (users: Array) => { return users.map(u => ({ id: typeof u === 'string' ? u : u.id, username: 'user', })); }), - pack: jest.fn(async (user: string | { id: string }) => { + pack: vi.fn(async (user: string | { id: string }) => { return { id: typeof user === 'string' ? user : user.id, username: 'user', @@ -195,7 +195,7 @@ describe('DriveFileEntityService', () => { test('detail: true uses DriveFolderEntityService pack', async () => { const folder = await createFolder('packmany-folder', null); const file = await createFile(folder.id, null); - const packSpy = jest.spyOn(driveFolderEntityService, 'pack'); + const packSpy = vi.spyOn(driveFolderEntityService, 'pack'); await service.packMany([file], { detail: true, self: true }); expect(packSpy).toHaveBeenCalled(); diff --git a/packages/backend/test/unit/entities/DriveFolderEntityService.ts b/packages/backend/test/unit/entities/DriveFolderEntityService.ts index 299ee5f42b..9030cea3fe 100644 --- a/packages/backend/test/unit/entities/DriveFolderEntityService.ts +++ b/packages/backend/test/unit/entities/DriveFolderEntityService.ts @@ -5,7 +5,7 @@ process.env.NODE_ENV = 'test'; -import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { Test } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing'; import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js'; diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts index ca6a639be8..1b5f9ed874 100644 --- a/packages/backend/test/unit/entities/UserEntityService.ts +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -4,6 +4,7 @@ */ import { Test, TestingModule } from '@nestjs/testing'; +import { describe, expect, beforeAll, afterAll, test } from 'vitest'; import type { MiUser } from '@/models/User.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GlobalModule } from '@/GlobalModule.js'; @@ -248,6 +249,16 @@ describe('UserEntityService', () => { expect(actual.achievements).toEqual(achievements); }); + test('alsoKnownAs as string does not throw', async () => { + const me = await createUser(); + const who = await createUser(); + + const whoWithStringAlsoKnownAs: MiUser = { ...who, alsoKnownAs: 'https://remote.example.com/users/alice' as any }; + + const actual = await service.pack(whoWithStringAlsoKnownAs, me, { schema: 'UserDetailedNotMe' }) as any; + expect(Array.isArray(actual.alsoKnownAs)).toBe(true); + }); + describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { test('no-preload', async() => { const me = await createUser(); diff --git a/packages/backend/test/unit/extract-mentions.ts b/packages/backend/test/unit/extract-mentions.ts index 3403387e30..0d02b27111 100644 --- a/packages/backend/test/unit/extract-mentions.ts +++ b/packages/backend/test/unit/extract-mentions.ts @@ -4,6 +4,7 @@ */ import * as assert from 'assert'; +import { describe, test } from 'vitest'; import { parse } from 'mfm-js'; import { extractMentions } from '@/misc/extract-mentions.js'; diff --git a/packages/backend/test/unit/misc/check-word-mute.ts b/packages/backend/test/unit/misc/check-word-mute.ts index eb0ca0f6cf..756f503aef 100644 --- a/packages/backend/test/unit/misc/check-word-mute.ts +++ b/packages/backend/test/unit/misc/check-word-mute.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { describe, expect, it } from 'vitest'; import { checkWordMute } from '@/misc/check-word-mute.js'; describe(checkWordMute, () => { diff --git a/packages/backend/test/unit/misc/correct-filename.ts b/packages/backend/test/unit/misc/correct-filename.ts index c76fb4c494..fd792b83ca 100644 --- a/packages/backend/test/unit/misc/correct-filename.ts +++ b/packages/backend/test/unit/misc/correct-filename.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { describe, expect, it, test } from 'vitest'; import { correctFilename } from '@/misc/correct-filename.js'; describe(correctFilename, () => { diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts index d14efb10a6..ccb0fff4bf 100644 --- a/packages/backend/test/unit/misc/id.ts +++ b/packages/backend/test/unit/misc/id.ts @@ -4,7 +4,7 @@ */ import { ulid } from 'ulid'; -import { describe, expect, test } from '@jest/globals'; +import { describe, expect, test } from 'vitest'; import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js'; import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts index 3c628d8298..4be3b4992b 100644 --- a/packages/backend/test/unit/misc/is-renote.ts +++ b/packages/backend/test/unit/misc/is-renote.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { describe, expect, test } from 'vitest'; import { isQuote, isRenote } from '@/misc/is-renote.js'; import { MiNote } from '@/models/Note.js'; diff --git a/packages/backend/test/unit/misc/loader.ts b/packages/backend/test/unit/misc/loader.ts index 2cf54e1555..cfeb9b5b2e 100644 --- a/packages/backend/test/unit/misc/loader.ts +++ b/packages/backend/test/unit/misc/loader.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { expect, describe, it } from 'vitest'; import { DebounceLoader } from '@/misc/loader.js'; class Mock { diff --git a/packages/backend/test/unit/misc/others.ts b/packages/backend/test/unit/misc/others.ts index 3bc134a2b8..92f26ff41a 100644 --- a/packages/backend/test/unit/misc/others.ts +++ b/packages/backend/test/unit/misc/others.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, expect, test } from '@jest/globals'; +import { describe, expect, test } from 'vitest'; import { contentDisposition } from '@/misc/content-disposition.js'; describe('misc:content-disposition', () => { diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts index 1c463c82c6..f15750027e 100644 --- a/packages/backend/test/unit/misc/should-hide-note-by-time.ts +++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts @@ -3,12 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { describe, expect, test, beforeEach, afterEach } from '@jest/globals'; +import { describe, expect, test, beforeEach, afterEach } from 'vitest'; import * as lolex from '@sinonjs/fake-timers'; import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js'; describe('misc:should-hide-note-by-time', () => { - let clock: lolex.InstalledClock; + let clock: lolex.Clock; const epoch = Date.UTC(2000, 0, 1, 0, 0, 0); beforeEach(() => { diff --git a/packages/backend/test/unit/misc/ulid.ts b/packages/backend/test/unit/misc/ulid.ts new file mode 100644 index 0000000000..debca9bde9 --- /dev/null +++ b/packages/backend/test/unit/misc/ulid.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from 'vitest'; +import { parseUlidFull } from '@/misc/id/ulid.js'; + +// Timestamp part "01KPS7S300" encodes 1776816000000ms (2026-04-22T00:00:00.000Z) +// Verified: 1*32^8 + 19*32^7 + 22*32^6 + 25*32^5 + 7*32^4 + 25*32^3 + 3*32^2 = 1776816000000 + +describe('misc:ulid', () => { + test('parseUlidFull - timestamp is parsed correctly', () => { + // id[10..25] = all zeros (valid Crockford Base32) + // 2026-04-22T00:00:00.000Z + const { date } = parseUlidFull('01KPS7S3000000000000000000'); + expect(date).toBe(1776816000000); + }); + + test('parseUlidFull - W/X/Y/Z at id[10] (chunk 1 head) do not throw', () => { + // id[10] = W + expect(() => parseUlidFull('01KPS7S300W000000000000000')).not.toThrow(); + // id[10] = X + expect(() => parseUlidFull('01KPS7S300X000000000000000')).not.toThrow(); + // id[10] = Y + expect(() => parseUlidFull('01KPS7S300Y000000000000000')).not.toThrow(); + // id[10] = Z + expect(() => parseUlidFull('01KPS7S300Z000000000000000')).not.toThrow(); + }); + + test('parseUlidFull - W/X/Y/Z at id[16] (chunk 2 head) do not throw', () => { + // id[16] = W + expect(() => parseUlidFull('01KPS7S300ABCDEFW000000000')).not.toThrow(); + // id[16] = X + expect(() => parseUlidFull('01KPS7S300ABCDEFX000000000')).not.toThrow(); + // id[16] = Y + expect(() => parseUlidFull('01KPS7S300ABCDEFY000000000')).not.toThrow(); + // id[16] = Z + expect(() => parseUlidFull('01KPS7S300ABCDEFZ000000000')).not.toThrow(); + }); + + test('parseUlidFull - additional exceeds uint64 max (all-Z randomness)', () => { + // All 16 random chars = 'Z' (Crockford max) → 80-bit value > uint64 max + const { additional } = parseUlidFull('01ARZ3NDEKZZZZZZZZZZZZZZZZ'); + const uint64Max = 2n ** 64n - 1n; + expect(additional > uint64Max).toBe(true); + expect(BigInt.asUintN(64, additional) <= uint64Max).toBe(true); + }); +}); diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts index 01a36c9fef..7295ab7511 100644 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { jest } from '@jest/globals'; +import { describe, expect, test, beforeAll, beforeEach, afterEach, afterAll, vi } from 'vitest'; +import type { Mocked } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns'; @@ -24,7 +25,7 @@ const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0)); describe('CheckModeratorsActivityProcessorService', () => { let app: TestingModule; - let clock: lolex.InstalledClock; + let clock: lolex.Clock; let service: CheckModeratorsActivityProcessorService; // -------------------------------------------------------------------------------------- @@ -32,10 +33,10 @@ describe('CheckModeratorsActivityProcessorService', () => { let usersRepository: UsersRepository; let userProfilesRepository: UserProfilesRepository; let idService: IdService; - let roleService: jest.Mocked; - let announcementService: jest.Mocked; - let emailService: jest.Mocked; - let systemWebhookService: jest.Mocked; + let roleService: Mocked; + let announcementService: Mocked; + let emailService: Mocked; + let systemWebhookService: Mocked; let systemWebhook1: MiSystemWebhook; let systemWebhook2: MiSystemWebhook; @@ -94,30 +95,30 @@ describe('CheckModeratorsActivityProcessorService', () => { CheckModeratorsActivityProcessorService, IdService, { - provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }), + provide: RoleService, useFactory: () => ({ getModerators: vi.fn() }), }, { - provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), + provide: MetaService, useFactory: () => ({ fetch: vi.fn() }), }, { - provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }), + provide: AnnouncementService, useFactory: () => ({ create: vi.fn() }), }, { - provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), + provide: EmailService, useFactory: () => ({ sendEmail: vi.fn() }), }, { provide: SystemWebhookService, useFactory: () => ({ - fetchActiveSystemWebhooks: jest.fn(), - enqueueSystemWebhook: jest.fn(), + fetchActiveSystemWebhooks: vi.fn(), + enqueueSystemWebhook: vi.fn(), }), }, { provide: QueueLoggerService, useFactory: () => ({ logger: ({ createSubLogger: () => ({ - info: jest.fn(), - warn: jest.fn(), - succ: jest.fn(), + info: vi.fn(), + warn: vi.fn(), + succ: vi.fn(), }), }), }), @@ -131,10 +132,10 @@ describe('CheckModeratorsActivityProcessorService', () => { service = app.get(CheckModeratorsActivityProcessorService); idService = app.get(IdService); - roleService = app.get(RoleService) as jest.Mocked; - announcementService = app.get(AnnouncementService) as jest.Mocked; - emailService = app.get(EmailService) as jest.Mocked; - systemWebhookService = app.get(SystemWebhookService) as jest.Mocked; + roleService = app.get(RoleService) as Mocked; + announcementService = app.get(AnnouncementService) as Mocked; + emailService = app.get(EmailService) as Mocked; + systemWebhookService = app.get(SystemWebhookService) as Mocked; app.enableShutdownHooks(); }); diff --git a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts index 631e160afc..b773857bea 100644 --- a/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CleanRemoteNotesProcessorService.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { jest } from '@jest/globals'; +import { describe, expect, test, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import ms from 'ms'; import { @@ -44,8 +44,8 @@ describe('CleanRemoteNotesProcessorService', () => { // Mock job object const createMockJob = () => ({ - log: jest.fn(), - updateProgress: jest.fn(), + log: vi.fn(), + updateProgress: vi.fn(), }); async function createUser(data: Partial = {}) { @@ -96,9 +96,9 @@ describe('CleanRemoteNotesProcessorService', () => { useFactory: () => ({ logger: { createSubLogger: () => ({ - info: jest.fn(), - warn: jest.fn(), - succ: jest.fn(), + info: vi.fn(), + warn: vi.fn(), + succ: vi.fn(), }), }, }), @@ -125,7 +125,7 @@ describe('CleanRemoteNotesProcessorService', () => { beforeEach(() => { // Reset mocks - jest.clearAllMocks(); + vi.clearAllMocks(); // Set default meta values meta.enableRemoteNotesCleaning = true; diff --git a/packages/backend/test/unit/server/FileServerService.ts b/packages/backend/test/unit/server/FileServerService.ts index c88175c5c7..4c3335b902 100644 --- a/packages/backend/test/unit/server/FileServerService.ts +++ b/packages/backend/test/unit/server/FileServerService.ts @@ -7,7 +7,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import fastifyStatic from '@fastify/static'; import Fastify, { type FastifyInstance } from 'fastify'; -import { describe, expect, test } from '@jest/globals'; +import { describe, expect, test, beforeAll, afterAll, afterEach } from 'vitest'; import sharp from 'sharp'; import { DataSource, type Repository } from 'typeorm'; import { initTestDb, randomString } from '../../utils.js'; @@ -296,7 +296,7 @@ describe('FileServerService', () => { }); expect(res.statusCode).toBe(404); - expect(res.headers['cache-control']).toBe('max-age=86400'); + expect(res.headers['cache-control']).toBe('public, max-age=0'); }); test('GET /files/:key 画像配信ヘッダを検証する', async () => { diff --git a/packages/backend/test/unit/server/api/drive/files/create.ts b/packages/backend/test/unit/server/api/drive/files/create.ts deleted file mode 100644 index e86b818ca5..0000000000 --- a/packages/backend/test/unit/server/api/drive/files/create.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Test, TestingModule } from '@nestjs/testing'; -import { FastifyInstance } from 'fastify'; -import request from 'supertest'; -import { randomString } from '../../../../../utils.js'; -import { CoreModule } from '@/core/CoreModule.js'; -import { RoleService } from '@/core/RoleService.js'; -import { DI } from '@/di-symbols.js'; -import { GlobalModule } from '@/GlobalModule.js'; -import { DriveFoldersRepository, MiDriveFolder, MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js'; -import { MiUser } from '@/models/User.js'; -import { ServerModule } from '@/server/ServerModule.js'; -import { ServerService } from '@/server/ServerService.js'; -import { IdService } from '@/core/IdService.js'; - -// TODO: uploadableFileTypes で許可されていないファイルが弾かれるかのテスト - -describe('/drive/files/create', () => { - let module: TestingModule; - let server: FastifyInstance; - let roleService: RoleService; - let idService: IdService; - - let root: MiUser; - let role_tinyAttachment: MiRole; - let role_imageOnly: MiRole; - let role_allowAllTypes: MiRole; - - let folder: MiDriveFolder; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [GlobalModule, CoreModule, ServerModule], - }).compile(); - module.enableShutdownHooks(); - - const serverService = module.get(ServerService); - await serverService.launch(); - server = serverService.fastify; - - idService = module.get(IdService); - - const usersRepository = module.get(DI.usersRepository); - await usersRepository.createQueryBuilder().delete().execute(); - root = await usersRepository.insert({ - id: idService.gen(), - username: 'root', - usernameLower: 'root', - token: '1234567890123456', - }).then(x => usersRepository.findOneByOrFail(x.identifiers[0])); - - const userProfilesRepository = module.get(DI.userProfilesRepository); - await userProfilesRepository.createQueryBuilder().delete().execute(); - await userProfilesRepository.insert({ - userId: root.id, - }); - - const driveFoldersRepository = module.get(DI.driveFoldersRepository); - folder = await driveFoldersRepository.insertOne({ - id: idService.gen(), - name: 'root-folder', - parentId: null, - userId: root.id, - }); - - roleService = module.get(RoleService); - role_imageOnly = await roleService.create({ - name: 'test-role001', - description: 'Test role001 description', - target: 'manual', - policies: { - uploadableFileTypes: { - useDefault: false, - priority: 1, - value: ['image/png'], - }, - }, - }); - role_allowAllTypes = await roleService.create({ - name: 'test-role002', - description: 'Test role002 description', - target: 'manual', - policies: { - uploadableFileTypes: { - useDefault: false, - priority: 1, - value: ['*/*'], - }, - }, - }); - role_tinyAttachment = await roleService.create({ - name: 'test-role003', - description: 'Test role003 description', - target: 'manual', - policies: { - maxFileSizeMb: { - useDefault: false, - priority: 1, - // 10byte - value: 10 / 1024 / 1024, - }, - }, - }); - }); - - beforeEach(async () => { - await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => { - }); - await roleService.unassign(root.id, role_imageOnly.id).catch(() => { - }); - await roleService.unassign(root.id, role_allowAllTypes.id).catch(() => { - }); - }); - - afterAll(async () => { - await server.close(); - await module.close(); - }); - - async function postFile(props: { - name: string, - comment: string, - isSensitive: boolean, - force: boolean, - fileContent: Buffer | string, - }) { - const { name, comment, isSensitive, force, fileContent } = props; - - return await request(server.server) - .post('/api/drive/files/create') - .set('Content-Type', 'multipart/form-data') - .attach('file', fileContent) - .field('name', name) - .field('comment', comment) - .field('isSensitive', isSensitive) - .field('force', force) - .field('folderId', folder.id) - .field('i', root.token ?? ''); - } - - test('200 ok (all types allowed)', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(1000 * 1000)), - }); - expect(result.statusCode).toBe(200); - expect(result.body.name).toBe(name + '.unknown'); - expect(result.body.comment).toBe(comment); - expect(result.body.isSensitive).toBe(true); - expect(result.body.folderId).toBe(folder.id); - }); - - test('400 when not allowed type', async () => { - await roleService.assign(root.id, role_imageOnly.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(10)), - }); - expect(result.statusCode).toBe(400); - expect(result.body.error.code).toBe('UNALLOWED_FILE_TYPE'); - }); - - test('200 ok (with size limited role)', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - await roleService.assign(root.id, role_tinyAttachment.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(10)), - }); - expect(result.statusCode).toBe(200); - expect(result.body.name).toBe(name + '.unknown'); - expect(result.body.comment).toBe(comment); - expect(result.body.isSensitive).toBe(true); - expect(result.body.folderId).toBe(folder.id); - }); - - test('413 too large', async () => { - await roleService.assign(root.id, role_allowAllTypes.id); - await roleService.assign(root.id, role_tinyAttachment.id); - - const name = randomString(); - const comment = randomString(); - const result = await postFile({ - name: name, - comment: comment, - isSensitive: true, - force: true, - fileContent: Buffer.from('a'.repeat(11)), - }); - expect(result.statusCode).toBe(413); - expect(result.body.error.code).toBe('MAX_FILE_SIZE_EXCEEDED'); - }); -}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index f91fb7f9b1..e9938c5fa8 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -9,13 +9,13 @@ import { basename, isAbsolute } from 'node:path'; import { randomUUID } from 'node:crypto'; import { inspect } from 'node:util'; import WebSocket, { ClientOptions } from 'ws'; -import fetch, { File, RequestInit, type Headers } from 'node-fetch'; +import fetch, { RequestInit, type Headers } from 'node-fetch'; import * as htmlParser from 'node-html-parser'; import { DataSource } from 'typeorm'; import { type Response } from 'node-fetch'; import Fastify from 'fastify'; -import { entities } from '../src/postgres.js'; -import { loadConfig } from '../src/config.js'; +import { entities } from '@/postgres.js'; +import { loadConfig } from '@/config.js'; import type * as misskey from 'misskey-js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; diff --git a/packages/backend/vitest.config.e2e.ts b/packages/backend/vitest.config.e2e.ts new file mode 100644 index 0000000000..cbfc0a288c --- /dev/null +++ b/packages/backend/vitest.config.e2e.ts @@ -0,0 +1,13 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; +import { baseConfig } from './vitest.config.js'; + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ['./test/e2e/**/*.ts'], + globalSetup: './built-test/entry.js', + setupFiles: ['./test/setup.e2e.ts'], + }, + }), +); diff --git a/packages/backend/vitest.config.fed.ts b/packages/backend/vitest.config.fed.ts new file mode 100644 index 0000000000..fcd58f8abb --- /dev/null +++ b/packages/backend/vitest.config.fed.ts @@ -0,0 +1,11 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; +import { baseConfig } from './vitest.config.js'; + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + include: ['test-federation/test/**/*.test.ts'], + }, + }), +); diff --git a/packages/backend/vitest.config.ts b/packages/backend/vitest.config.ts new file mode 100644 index 0000000000..cfcf8c776f --- /dev/null +++ b/packages/backend/vitest.config.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from 'node:events'; +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +// Raise the global EventEmitter listener limit before Vitest wires CLI listeners. +EventEmitter.defaultMaxListeners = 20; + +export const baseConfig = defineConfig({ + test: { + dir: import.meta.dirname, + exclude: ['node_modules', 'dist'], + coverage: { + provider: 'v8', + reportsDirectory: 'coverage', + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts'], + }, + restoreMocks: true, + testTimeout: 60000, + maxWorkers: 1, + logHeapUsage: true, + vmMemoryLimit: 1024, + maxConcurrency: 32, + }, + resolve: { + alias: { + '@': resolve(__dirname, './src'), + }, + }, +}); + +export default baseConfig; diff --git a/packages/backend/vitest.config.unit.ts b/packages/backend/vitest.config.unit.ts new file mode 100644 index 0000000000..8f341fdc09 --- /dev/null +++ b/packages/backend/vitest.config.unit.ts @@ -0,0 +1,12 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; +import { baseConfig } from './vitest.config.js'; + +export default mergeConfig( + baseConfig, + defineConfig({ + test: { + globalSetup: './test/setup.unit.ts', + include: ['test/unit/**/*.ts', 'src/**/*.test.ts'], + }, + }), +); diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 28bcc47d63..f19e901b34 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,16 +11,16 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "rollup": "4.60.0" + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "rollup": "4.60.1" }, "dependencies": { "estree-walker": "3.0.3", "i18n": "workspace:*", "magic-string": "0.30.21", - "rolldown": "1.0.0-rc.11", - "vite": "8.0.2" + "rolldown": "1.0.0-rc.15", + "vite": "8.0.8" } } diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 3bb8e00d59..9b53d369d2 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -14,7 +14,7 @@ "@rollup/plugin-json": "6.1.0", "@rollup/pluginutils": "5.3.0", "@twemoji/parser": "16.0.0", - "@vitejs/plugin-vue": "6.0.5", + "@vitejs/plugin-vue": "6.0.6", "buraha": "0.0.1", "estree-walker": "3.0.3", "frontend-shared": "workspace:*", @@ -24,40 +24,39 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.60.0", - "shiki": "3.23.0", + "rollup": "4.60.1", + "shiki": "4.0.2", "tinycolor2": "1.6.0", "uuid": "13.0.0", - "vue": "3.5.30" + "vue": "3.5.32" }, "devDependencies": { - "@misskey-dev/summaly": "5.2.5", + "@misskey-dev/summaly": "5.3.0", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.10", - "@types/node": "24.12.0", + "@types/node": "24.12.2", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "@vitest/coverage-v8": "4.1.1", - "@vue/runtime-core": "3.5.30", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@vitest/coverage-v8": "4.1.4", + "@vue/runtime-core": "3.5.32", "acorn": "8.16.0", "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.8.0", - "happy-dom": "20.8.8", + "happy-dom": "20.9.0", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.12.14", - "nodemon": "3.1.14", - "prettier": "3.8.1", - "sass-embedded": "1.98.0", - "start-server-and-test": "2.1.5", + "msw": "2.13.3", + "prettier": "3.8.3", + "sass-embedded": "1.99.0", + "start-server-and-test": "3.0.2", "tsx": "4.21.0", - "vite": "8.0.2", + "vite": "8.0.8", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "3.2.6", "vue-eslint-parser": "10.4.0", diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index c53695735f..60c630af14 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,10 +21,10 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "esbuild": "0.27.4", + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "esbuild": "0.28.0", "eslint-plugin-vue": "10.8.0", "nodemon": "3.1.14", "vue-eslint-parser": "10.4.0" @@ -35,6 +35,6 @@ "dependencies": { "i18n": "workspace:*", "misskey-js": "workspace:*", - "vue": "3.5.30" + "vue": "3.5.32" } } diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json index 6b1804a0fc..c2600dc45e 100644 --- a/packages/frontend-shared/tsconfig.json +++ b/packages/frontend-shared/tsconfig.json @@ -7,7 +7,7 @@ "declaration": true, "declarationMap": true, "sourceMap": false, - "outDir": "./js-built/", + "outDir": "./js-built", "removeComments": true, "resolveJsonModule": true, "strict": true, @@ -17,6 +17,7 @@ "noImplicitReturns": true, "esModuleInterop": true, "verbatimModuleSyntax": true, + "rootDir": "./js", "paths": { "@/*": ["./*"], "@@/*": ["./*"] diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 7bcf097ec4..18ccc43f07 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -21,11 +21,11 @@ "@github/webauthn-json": "2.1.1", "@mcaptcha/core-glue": "0.1.0-alpha-5", "@misskey-dev/browser-image-resizer": "2024.1.0", - "@sentry/vue": "10.40.0", + "@sentry/vue": "10.48.0", "@syuilo/aiscript": "1.2.1", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", - "@vitejs/plugin-vue": "6.0.5", + "@vitejs/plugin-vue": "6.0.6", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.16", "analytics": "0.8.19", "broadcast-channel": "7.3.0", @@ -38,11 +38,11 @@ "chartjs-plugin-zoom": "2.2.0", "chromatic": "15.3.1", "compare-versions": "6.1.1", - "cropperjs": "2.1.0", + "cropperjs": "2.1.1", "date-fns": "4.1.0", "eventemitter3": "5.0.4", "execa": "9.6.1", - "exifreader": "4.37.0", + "exifreader": "4.38.1", "frontend-shared": "workspace:*", "i18n": "workspace:*", "icons-subsetter": "workspace:*", @@ -61,23 +61,23 @@ "punycode.js": "2.3.1", "qr-code-styling": "1.9.2", "qr-scanner": "1.4.2", - "sanitize-html": "2.17.1", - "shiki": "3.23.0", + "sanitize-html": "2.17.3", + "shiki": "4.0.2", "textarea-caret": "3.1.0", "three": "0.183.2", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "v-code-diff": "1.13.1", - "vue": "3.5.30", + "vue": "3.5.32", "wanakana": "5.3.1" }, "devDependencies": { - "@misskey-dev/summaly": "5.2.5", + "@misskey-dev/summaly": "5.3.0", "@rollup/plugin-json": "6.1.0", "@rollup/pluginutils": "5.3.0", "@storybook/addon-essentials": "8.6.18", "@storybook/addon-interactions": "8.6.18", - "@storybook/addon-links": "10.3.3", + "@storybook/addon-links": "10.3.5", "@storybook/addon-mdx-gfm": "8.6.18", "@storybook/addon-storysource": "8.6.18", "@storybook/blocks": "8.6.18", @@ -85,13 +85,13 @@ "@storybook/core-events": "8.6.18", "@storybook/manager-api": "8.6.18", "@storybook/preview-api": "8.6.18", - "@storybook/react": "10.3.3", - "@storybook/react-vite": "10.3.3", + "@storybook/react": "10.3.5", + "@storybook/react-vite": "10.3.5", "@storybook/test": "8.6.18", "@storybook/theming": "8.6.18", "@storybook/types": "8.6.18", - "@storybook/vue3": "10.3.3", - "@storybook/vue3-vite": "10.3.3", + "@storybook/vue3": "10.3.5", + "@storybook/vue3-vite": "10.3.5", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", @@ -99,45 +99,45 @@ "@types/insert-text-at-cursor": "0.3.2", "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.10", - "@types/node": "24.12.0", + "@types/node": "24.12.2", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.16.1", "@types/seedrandom": "3.0.8", "@types/textarea-caret": "3.0.4", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "@vitest/coverage-v8": "4.1.1", - "@vue/compiler-core": "3.5.30", + "@typescript-eslint/eslint-plugin": "8.58.2", + "@typescript-eslint/parser": "8.58.2", + "@vitest/coverage-v8": "4.1.4", + "@vue/compiler-core": "3.5.32", "acorn": "8.16.0", "astring": "1.9.0", "cross-env": "10.1.0", - "cypress": "15.13.0", + "cypress": "15.13.1", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.8.0", "estree-walker": "3.0.3", - "happy-dom": "20.8.8", + "happy-dom": "20.9.0", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "minimatch": "10.2.4", - "msw": "2.12.14", - "msw-storybook-addon": "2.0.6", + "minimatch": "10.2.5", + "msw": "2.13.3", + "msw-storybook-addon": "2.0.7", "nodemon": "3.1.14", - "prettier": "3.8.1", - "react": "19.2.4", - "react-dom": "19.2.4", - "rolldown": "1.0.0-rc.11", - "sass-embedded": "1.98.0", + "prettier": "3.8.3", + "react": "19.2.5", + "react-dom": "19.2.5", + "rolldown": "1.0.0-rc.15", + "sass-embedded": "1.99.0", "seedrandom": "3.0.5", - "start-server-and-test": "2.1.5", - "storybook": "10.3.3", + "start-server-and-test": "3.0.2", + "storybook": "10.3.5", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "tsx": "4.21.0", - "vite": "8.0.2", - "vite-plugin-glsl": "1.5.6", + "vite": "8.0.8", + "vite-plugin-glsl": "1.6.0", "vite-plugin-turbosnap": "1.0.3", - "vitest": "4.1.1", + "vitest": "4.1.4", "vitest-fetch-mock": "0.4.5", "vue-component-type-helpers": "3.2.6", "vue-eslint-parser": "10.4.0", diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 854ed31ed5..d7a622f1d9 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -4,14 +4,12 @@ SPDX-License-Identifier: AGPL-3.0-only --> - @@ -19,34 +17,20 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - - - - + diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index c78cc44425..ba68971034 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -263,7 +263,7 @@ const emit = defineEmits<{ const inTimeline = inject('inTimeline', false); const tl_withSensitive = inject>('tl_withSensitive', ref(true)); -const inChannel = inject('inChannel', null); +const inChannel = inject(DI.inChannel, null); const currentClip = inject | null>('currentClip', null); let note = deepClone(props.note); @@ -650,23 +650,35 @@ async function showRenoteMenu() { }; } - const renoteDetailsMenu: MenuItem = { + const renoteDetailsMenu: MenuItem[] = [{ type: 'link', text: i18n.ts.renoteDetails, icon: 'ti ti-info-circle', to: notePage(note), - }; + }]; + + if ( + props.note.channelId != null && + (inChannel == null || props.note.channelId !== inChannel.value) + ) { + renoteDetailsMenu.push({ + type: 'link', + text: i18n.ts.viewRenotedChannel, + icon: 'ti ti-device-tv', + to: `/channels/${props.note.channelId}`, + }); + } if (isMyRenote) { os.popupMenu([ - renoteDetailsMenu, + ...renoteDetailsMenu, getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ - renoteDetailsMenu, + ...renoteDetailsMenu, getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), { type: 'divider' }, getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 083e3e5da0..114edc6204 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -238,6 +238,7 @@ import { isLink } from '@@/js/is-link.js'; import { host } from '@@/js/config.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { Keymap } from '@/utility/hotkey.js'; +import type { MenuItem } from '@/types/menu.js'; import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkReactionsViewer from '@/components/MkReactionsViewer.vue'; @@ -286,7 +287,7 @@ const props = withDefaults(defineProps<{ initialTab: 'replies', }); -const inChannel = inject('inChannel', null); +const inChannel = inject(DI.inChannel, null); let note = deepClone(props.note); @@ -581,18 +582,36 @@ async function showRenoteMenu() { const isLoggedIn = await pleaseLogin({ openOnRemote: pleaseLoginContext.value }); if (!isLoggedIn) return; - os.popupMenu([{ - text: i18n.ts.unrenote, - icon: 'ti ti-trash', - danger: true, - action: () => { - misskeyApi('notes/delete', { - noteId: note.id, - }).then(() => { - globalEvents.emit('noteDeleted', note.id); - }); - }, - }], renoteTime.value); + const menu: MenuItem[] = []; + + if (isMyRenote) { + menu.push({ + text: i18n.ts.unrenote, + icon: 'ti ti-trash', + danger: true, + action: () => { + misskeyApi('notes/delete', { + noteId: note.id, + }).then(() => { + globalEvents.emit('noteDeleted', note.id); + }); + }, + }); + } + + if ( + props.note.channelId != null && + (inChannel == null || props.note.channelId !== inChannel.value) + ) { + menu.push({ + type: 'link', + text: i18n.ts.viewRenotedChannel, + icon: 'ti ti-device-tv', + to: `/channels/${props.note.channelId}`, + }); + } + + os.popupMenu(menu, renoteTime.value); } function focus() { diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue index 4b6467fdda..f2acec32f0 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue new file mode 100644 index 0000000000..ae36f4e279 --- /dev/null +++ b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue @@ -0,0 +1,93 @@ + + + + + + + + + + + {{ i18n.ts._role.useBaseValue }} + + + + + + + {{ i18n.ts._role.useBaseValue }} + + + + + + {{ i18n.ts._role.priority }} + + + + + + + + diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.vue b/packages/frontend/src/pages/admin/roles.policy-editor.vue new file mode 100644 index 0000000000..f93cb703e6 --- /dev/null +++ b/packages/frontend/src/pages/admin/roles.policy-editor.vue @@ -0,0 +1,471 @@ + + + + + + {{ i18n.ts._role._options.rateLimitFactor }} + {{ Math.floor(valuesModel.rateLimitFactor * 100) }}% + + + {{ i18n.ts._role._options.descriptionOfRateLimitFactor }} + + + + + + {{ i18n.ts._role._options.gtlAvailable }} + {{ valuesModel.gtlAvailable ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.ltlAvailable }} + {{ valuesModel.ltlAvailable ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canPublicNote }} + {{ valuesModel.canPublicNote ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.chatAvailability }} + {{ valuesModel.chatAvailability === 'available' ? i18n.ts.yes : valuesModel.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.mentionMax }} + {{ valuesModel.mentionLimit }} + + + + + + + + {{ i18n.ts._role._options.canInvite }} + {{ valuesModel.canInvite ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.inviteLimit }} + {{ valuesModel.inviteLimit }} + + + + + + + + {{ i18n.ts._role._options.inviteLimitCycle }} + {{ valuesModel.inviteLimitCycle + i18n.ts._time.minute }} + + + {{ i18n.ts._time.minute }} + + + + + + {{ i18n.ts._role._options.inviteExpirationTime }} + {{ valuesModel.inviteExpirationTime + i18n.ts._time.minute }} + + + {{ i18n.ts._time.minute }} + + + + + + {{ i18n.ts._role._options.canManageAvatarDecorations }} + {{ valuesModel.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canManageCustomEmojis }} + {{ valuesModel.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canSearchNotes }} + {{ valuesModel.canSearchNotes ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canSearchUsers }} + {{ valuesModel.canSearchUsers ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canUseTranslator }} + {{ valuesModel.canUseTranslator ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.driveCapacity }} + {{ valuesModel.driveCapacityMb }}MB + + + MB + + + + + + {{ i18n.ts._role._options.maxFileSize }} + {{ valuesModel.maxFileSizeMb }}MB + + + MB + + {{ i18n.ts._role._options.maxFileSize_caption }} + + + + + + + {{ i18n.ts._role._options.uploadableFileTypes }} + ... + + valuesModel.uploadableFileTypes = v.split('\n')"> + + {{ i18n.ts._role._options.uploadableFileTypes_caption }} + {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }} + + + + + + + {{ i18n.ts._role._options.alwaysMarkNsfw }} + {{ valuesModel.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canUpdateBioMedia }} + {{ valuesModel.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.pinMax }} + {{ valuesModel.pinLimit }} + + + + + + + + {{ i18n.ts._role._options.antennaMax }} + {{ valuesModel.antennaLimit }} + + + + + + + + {{ i18n.ts._role._options.wordMuteMax }} + {{ valuesModel.wordMuteLimit }} + + + chars + + + + + + {{ i18n.ts._role._options.webhookMax }} + {{ valuesModel.webhookLimit }} + + + + + + + + {{ i18n.ts._role._options.clipMax }} + {{ valuesModel.clipLimit }} + + + + + + + + {{ i18n.ts._role._options.noteEachClipsMax }} + {{ valuesModel.noteEachClipsLimit }} + + + + + + + + {{ i18n.ts._role._options.userListMax }} + {{ valuesModel.userListLimit }} + + + + + + + + {{ i18n.ts._role._options.userEachUserListsMax }} + {{ valuesModel.userEachUserListsLimit }} + + + + + + + + {{ i18n.ts._role._options.canHideAds }} + {{ valuesModel.canHideAds ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.avatarDecorationLimit }} + {{ valuesModel.avatarDecorationLimit }} + + + + + + + + {{ i18n.ts._role._options.canImportAntennas }} + {{ valuesModel.canImportAntennas ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canImportBlocking }} + {{ valuesModel.canImportBlocking ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canImportFollowing }} + {{ valuesModel.canImportFollowing ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canImportMuting }} + {{ valuesModel.canImportMuting ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.canImportUserLists }} + {{ valuesModel.canImportUserLists ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + {{ i18n.ts._role._options.noteDraftLimit }} + {{ valuesModel.noteDraftLimit }} + + + + + + + + {{ i18n.ts._role._options.scheduledNoteLimit }} + {{ valuesModel.scheduledNoteLimit }} + + + + + + + + {{ i18n.ts._role._options.watermarkAvailable }} + {{ valuesModel.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }} + + + {{ i18n.ts.enable }} + + + + + + + + + diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index efd0df5579..d7f8ef151e 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -120,6 +120,7 @@ async function assign() { await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt: res.expiresAt }); //role.users.push(user); + usersPaginator.reload(); } async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) { @@ -130,6 +131,7 @@ async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) { action: async () => { await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: userId }); //role.users = role.users.filter(u => u.id !== userId); + usersPaginator.reload(); }, }], ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index e65a3c5ba8..94fc75657a 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -17,310 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only - - {{ i18n.ts._role._options.rateLimitFactor }} - {{ Math.floor(policies.rateLimitFactor * 100) }}% - policies.rateLimitFactor = (v / 100)"> - {{ i18n.ts._role._options.descriptionOfRateLimitFactor }} - - - - - {{ i18n.ts._role._options.gtlAvailable }} - {{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.ltlAvailable }} - {{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canPublicNote }} - {{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.chatAvailability }} - {{ policies.chatAvailability === 'available' ? i18n.ts.yes : policies.chatAvailability === 'readonly' ? i18n.ts.readonly : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.mentionMax }} - {{ policies.mentionLimit }} - - - - - - {{ i18n.ts._role._options.canInvite }} - {{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.inviteLimit }} - {{ policies.inviteLimit }} - - - - - - {{ i18n.ts._role._options.inviteLimitCycle }} - {{ policies.inviteLimitCycle + i18n.ts._time.minute }} - - {{ i18n.ts._time.minute }} - - - - - {{ i18n.ts._role._options.inviteExpirationTime }} - {{ policies.inviteExpirationTime + i18n.ts._time.minute }} - - {{ i18n.ts._time.minute }} - - - - - {{ i18n.ts._role._options.canManageAvatarDecorations }} - {{ policies.canManageAvatarDecorations ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canManageCustomEmojis }} - {{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canSearchNotes }} - {{ policies.canSearchNotes ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canSearchUsers }} - {{ policies.canSearchUsers ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canUseTranslator }} - {{ policies.canUseTranslator ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.driveCapacity }} - {{ policies.driveCapacityMb }}MB - - MB - - - - - {{ i18n.ts._role._options.maxFileSize }} - {{ policies.maxFileSizeMb }}MB - - MB - - {{ i18n.ts._role._options.maxFileSize_caption }} - - - - - - {{ i18n.ts._role._options.uploadableFileTypes }} - ... - policies.uploadableFileTypes = v.split('\n')"> - - {{ i18n.ts._role._options.uploadableFileTypes_caption }} - {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }} - - - - - - {{ i18n.ts._role._options.alwaysMarkNsfw }} - {{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canUpdateBioMedia }} - {{ policies.canUpdateBioMedia ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.pinMax }} - {{ policies.pinLimit }} - - - - - - {{ i18n.ts._role._options.antennaMax }} - {{ policies.antennaLimit }} - - - - - - {{ i18n.ts._role._options.wordMuteMax }} - {{ policies.wordMuteLimit }} - - chars - - - - - {{ i18n.ts._role._options.webhookMax }} - {{ policies.webhookLimit }} - - - - - - {{ i18n.ts._role._options.clipMax }} - {{ policies.clipLimit }} - - - - - - {{ i18n.ts._role._options.noteEachClipsMax }} - {{ policies.noteEachClipsLimit }} - - - - - - {{ i18n.ts._role._options.userListMax }} - {{ policies.userListLimit }} - - - - - - {{ i18n.ts._role._options.userEachUserListsMax }} - {{ policies.userEachUserListsLimit }} - - - - - - {{ i18n.ts._role._options.canHideAds }} - {{ policies.canHideAds ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.avatarDecorationLimit }} - {{ policies.avatarDecorationLimit }} - - - - - - {{ i18n.ts._role._options.canImportAntennas }} - {{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canImportBlocking }} - {{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canImportFollowing }} - {{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canImportMuting }} - {{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.canImportUserLists }} - {{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - - - - {{ i18n.ts._role._options.noteDraftLimit }} - {{ policies.noteDraftLimit }} - - - - - - {{ i18n.ts._role._options.scheduledNoteLimit }} - {{ policies.scheduledNoteLimit }} - - - - - - {{ i18n.ts._role._options.watermarkAvailable }} - {{ policies.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }} - - {{ i18n.ts.enable }} - - + {{ i18n.ts._role.new }} @@ -345,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only