Merge pull request #17351 from misskey-dev/develop

Release: 2026.5.0
This commit is contained in:
misskey-release-bot[bot] 2026-05-02 03:30:56 +00:00 committed by GitHub
commit 6391a4e7e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
246 changed files with 8245 additions and 8600 deletions

View file

@ -19,7 +19,6 @@
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"Vue.volar",
"Orta.vscode-jest",
"dbaeumer.vscode-eslint",
"mrmlnc.vscode-json5"
]

View file

@ -34,9 +34,6 @@ updates:
patterns:
- "storybook*"
- "@storybook/*"
swc-core:
patterns:
- "@swc/core*"
typescript-eslint:
patterns:
- "@typescript-eslint/*"

View file

@ -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

View file

@ -1,3 +1,39 @@
## 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
@ -31,7 +67,7 @@
- `users/following``birthday` プロパティは非推奨になりました。代わりに `users/get-following-users-by-birthday` をご利用ください。
### General
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
- Enhance: 「もうすぐ誕生日のユーザー」ウィジェットで、誕生日が至近のユーザーも表示できるように
(Cherry-picked from https://github.com/MisskeyIO/misskey)
- 「今日誕生日のユーザー」は「もうすぐ誕生日のユーザー」に名称変更されました
- Fix: ユーザーハッシュタグページでユーザーの読み込みが重複する問題を修正
@ -87,9 +123,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
@ -134,11 +170,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: 初回読み込み時にエラーになることがある問題を修正
@ -148,12 +184,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
@ -196,7 +232,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
## 2025.10.1
### General
- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
- Enhance: リモートユーザーに付与したロールバッジを表示できるように(オプトイン)
パフォーマンス上の問題からデフォルトで無効化されています。「コントロールパネル > パフォーマンス」から有効化できます。
- 依存関係の更新
@ -323,7 +359,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: チャンネルのハイライトページにノートが表示されない問題を修正
@ -483,7 +519,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: ヒントとコツを追加
@ -572,7 +608,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: システムアカウントの名前がサーバー名と同期されない問題を修正
@ -678,7 +714,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### General
- Enhance: プロキシアカウントをシステムアカウントとして作成するように
- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
- Enhance: OAuthで外部アプリからロゴが提供されている場合、それを表示できるように
書式は https://indieauth.spec.indieweb.org/20220212/#example-2 に準じます。
- Fix: システムアカウントが削除できる問題を修正
@ -692,7 +728,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Server
- Fix: 特定のケースでActivityPubの処理がデッドロックになることがあるのを修正
- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
- Fix: S3互換オブジェクトストレージでファイルのアップロードに失敗することがある問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/895)
@ -713,7 +749,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`
@ -730,7 +766,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でない文字列を使えないように
@ -740,12 +776,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にスラッシュが含まれている場合にページが正しく表示されない問題を修正
@ -772,14 +808,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
@ -787,26 +823,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に制限
@ -815,18 +851,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 環境で動作しない問題を修正
@ -852,12 +888,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の認可画面の改善
- どのアカウントで認証しようとしているのかがわかるように
- 認証するアカウントを切り替えられるように
@ -865,29 +901,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に紐づくリモートユーザーを照会できない
@ -895,18 +931,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: セキュリティに関する修正
@ -933,13 +969,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上で初期パスワードの入力欄を空欄にすると続行できます
- ユーザーデータを読み込む際の型が一部変更されました。
@ -959,7 +995,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
@ -977,7 +1013,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: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
@ -996,12 +1032,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 )
@ -1010,14 +1046,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: `<link rel="alternate">`を追って照会するのはOKレスポンスが返却された場合のみに
- Fix: `<link rel="alternate">`を追って照会するのはOKレスポンスが返却された場合のみに
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/633)
- Fix: メールにスタイルが適用されていなかった問題を修正
@ -1046,15 +1082,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)
@ -1086,9 +1122,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`)
@ -1097,7 +1133,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: ブラウザのコンテキストメニューを使用できるように
@ -1105,19 +1141,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)
@ -1160,14 +1196,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`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
@ -1194,7 +1230,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)
@ -1213,7 +1249,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Client
- Feat: アップロードするファイルの名前をランダム文字列にできるように
- Feat: 個別のお知らせにリンクで飛べるように
- Feat: 個別のお知らせにリンクで飛べるように
(Based on https://github.com/MisskeyIO/misskey/pull/639)
- Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように
- Enhance: 広告がMisskeyと同一ドメインの場合はRouterで遷移するように
@ -1243,9 +1279,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`を修正してください。
@ -1268,13 +1304,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リクエストされた場合に正しく応答できないのを修正
@ -1467,10 +1503,10 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### Note
- 依存関係の更新に伴い、Node.js 20.10.0が最小要件になりました
- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
**影響:**
それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。
**影響:**
それにより、投稿フォームから表示される絵文字ピッカーのピン留め絵文字がリセットされたように感じるかもしれません(新設された"ピン留め(全般)"の設定が使われるため)。
投稿用のピン留め絵文字をアップデート前の状態にするには、以下の手順で操作します。
1. 「設定」メニューに移動し、「絵文字ピッカー」タブを選択します。
@ -1517,7 +1553,7 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
- Enhance: Unicode 15.0のサポート
- Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように
- MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました
- 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります
- 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります
(例: ` ```js ` → Javascript, ` ```ais ` → AiScript
- Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように
- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
@ -1924,9 +1960,9 @@ v2025.12.0で行われた「configの`trustProxy`のデフォルト値を`false`
### General
- 招待機能を改善しました
* 過去に発行した招待コードを確認できるようになりました
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
* 過去に発行した招待コードを確認できるようになりました
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
@ -2089,9 +2125,9 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
* 「フォロワーのみ」の投稿は検索結果に表示されません。
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
- ユーザーへの自分用メモ機能
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
(自分自身に対してもメモを追加できます。)
* ユーザーメニューから追加できます。
* ユーザーメニューから追加できます。
デスクトップ表示ではusernameの右側のボタンからも追加可能
- チャンネルに色を設定できるようになりました。各ノートに設定した色のインジケーターが表示されます。
- チャンネルをアーカイブできるようになりました。

View file

@ -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 . ./

View file

@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": ["dom", "es5"],
"target": "es5",
"lib": ["dom"],
"target": "esnext",
"types": ["cypress", "node"]
},
"include": ["./**/*.ts"]

View file

@ -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."

View file

@ -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:

View file

@ -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 <b>Misskey</b>."
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"

View file

@ -1408,6 +1408,7 @@ frame: "フレーム"
presets: "プリセット"
zeroPadding: "ゼロ埋め"
nothingToConfigure: "設定項目はありません"
viewRenotedChannel: "リノート先のチャンネルを見る"
_imageEditing:
_vars:

View file

@ -1408,6 +1408,7 @@ frame: "프레임"
presets: "프리셋"
zeroPadding: "0으로 채우기"
nothingToConfigure: "설정 항목이 없습니다."
viewRenotedChannel: "리노트된 채널 보기"
_imageEditing:
_vars:
caption: "파일 설명"

View file

@ -5,6 +5,7 @@ introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນຊອ
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"Misskey instance\")"
monthAndDay: "ເດືອນ{month} / ວັນ{day}"
search: "ຄົ້ນຫາ"
reset: "ຣີເຊັດ"
notifications: "ການແຈ້ງເຕືອນ"
username: "ຊື່ຜູ້ໃຊ້"
password: "ລະຫັດຜ່ານ"

View file

@ -3401,6 +3401,8 @@ _imageEffector:
threshold: "เทรชโฮลด์"
centerX: "กลาง X"
centerY: "กลาง Y"
density: "ความหนาทึบ"
zoomLinesOutlineThickness: "ความหนาของเงาเส้น"
zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง"
circle: "ทรงกลม"
drafts: "ร่าง"

View file

@ -1408,6 +1408,7 @@ frame: "边框"
presets: "预设值"
zeroPadding: "填充 0"
nothingToConfigure: "没有项目"
viewRenotedChannel: "查看转帖所属频道"
_imageEditing:
_vars:
caption: "文件标题"

View file

@ -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: "檔案標題"

View file

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2026.3.2",
"version": "2026.5.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.32.1",
"packageManager": "pnpm@10.33.0",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
@ -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,30 +53,30 @@
"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",
"tar": "7.5.11",
"terser": "5.46.0"
"postcss": "8.5.9",
"tar": "7.5.13",
"terser": "5.46.1"
},
"devDependencies": {
"@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.0",
"@typescript-eslint/parser": "8.57.0",
"@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.11.0",
"cypress": "15.13.1",
"eslint": "9.39.4",
"globals": "17.4.0",
"globals": "17.5.0",
"ncp": "2.0.0",
"pnpm": "10.32.1",
"start-server-and-test": "2.1.5",
"pnpm": "10.33.0",
"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",

View file

@ -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"
}

View file

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1,4 +0,0 @@
user-agent: *
allow: /
# todo: sitemap

View file

@ -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.`);
}

View file

@ -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: [
"<rootDir>"
],
// 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: [
"<rootDir>/test/unit/**/*.ts",
"<rootDir>/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,
};

View file

@ -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: "<rootDir>/built-test/entry.js",
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
testMatch: [
"<rootDir>/test/e2e/**/*.ts",
],
};

View file

@ -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: [
'<rootDir>/test-federation/test/**/*.test.ts',
],
};

View file

@ -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: "<rootDir>/test/jest.setup.unit.cjs",
testMatch: [
"<rootDir>/test/unit/**/*.ts",
"<rootDir>/src/**/*.test.ts",
],
};

View file

@ -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);
}
});

View file

@ -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"');
}
};

View file

@ -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';

View file

@ -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.18",
"@swc/core-darwin-x64": "1.15.18",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.15.18",
"@swc/core-linux-arm64-gnu": "1.15.18",
"@swc/core-linux-arm64-musl": "1.15.18",
"@swc/core-linux-x64-gnu": "1.15.18",
"@swc/core-linux-x64-musl": "1.15.18",
"@swc/core-win32-arm64-msvc": "1.15.18",
"@swc/core-win32-ia32-msvc": "1.15.18",
"@swc/core-win32-x64-msvc": "1.15.18",
"@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.1008.0",
"@aws-sdk/lib-storage": "3.1008.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.1",
"@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",
"@napi-rs/canvas": "0.1.96",
"@nestjs/common": "11.1.16",
"@nestjs/core": "11.1.16",
"@nestjs/testing": "11.1.16",
"@napi-rs/canvas": "0.1.97",
"@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.43.0",
"@sentry/profiling-node": "10.43.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.4.16",
"@swc/cli": "0.8.0",
"@swc/core": "1.15.18",
"@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,44 +84,44 @@
"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.2",
"fastify": "5.8.5",
"fastify-raw-body": "5.0.0",
"feed": "5.2.0",
"file-type": "21.3.2",
"file-type": "22.0.1",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5",
"got": "14.6.6",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
"ioredis": "5.10.0",
"ioredis": "5.10.1",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0",
"is-svg": "6.1.0",
"json5": "2.2.3",
"jsonld": "9.0.0",
"juice": "11.1.1",
"meilisearch": "0.55.0",
"meilisearch": "0.57.0",
"mfm-js": "0.25.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.6",
"nanoid": "5.1.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "7.1.0",
"nodemailer": "8.0.2",
"nsfwjs": "4.2.0",
"nodemailer": "8.0.5",
"nsfwjs": "4.3.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
@ -152,19 +133,19 @@
"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.1",
"sanitize-html": "2.17.3",
"secure-json-parse": "4.1.0",
"semver": "7.7.4",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.31.4",
"systeminformation": "5.31.5",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@ -172,32 +153,31 @@
"ulid": "3.0.2",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.19.0",
"ws": "8.20.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.16",
"@sentry/vue": "10.43.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.18.0",
"@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
@ -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.0",
"@typescript-eslint/parser": "8.57.0",
"@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.0.1",
"pid-port": "2.1.1",
"rolldown": "1.0.0-rc.15",
"simple-oauth2": "5.1.0",
"supertest": "7.2.2",
"vite": "7.3.1"
"vite": "8.0.8",
"vitest": "4.1.4",
"vitest-mock-extended": "4.0.0"
}
}

View file

@ -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,
};
}
});

View file

@ -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();

View file

@ -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();
}
})
})();

View file

@ -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);

View file

@ -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,

View file

@ -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,
});

View file

@ -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(),
});

View file

@ -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();
}

View file

@ -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),

View file

@ -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 { NSFWJS, PredictionType } from 'nsfwjs';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
import type { Config } from '@/config.js';
import type { NSFWJS, PredictionType } from 'nsfwjs/core';
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
@ -43,10 +47,10 @@ export class AiService {
tf.env().global.fetch = fetch;
if (this.model == null) {
const nsfw = await import('nsfwjs');
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 });
}
});
}

View file

@ -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}`;
}

View file

@ -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);
}
}

View file

@ -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<MiLocalUser['id'], {
target: MiLocalUser['id'];
reason: NotificationType;
}[];
}>;
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,49 @@ class NotificationManager {
@bindThis
public async notify() {
for (const x of this.queue) {
if (this.queue.size === 0) {
return;
}
let visibleUserIds: Set<MiUser['id']> | null;
switch (this.note.visibility) {
case 'public':
case 'home':
visibleUserIds = null;
break;
case 'specified':
visibleUserIds = new Set(this.note.visibleUserIds);
break;
// TODO: フォロワー限定ノートにフォロワーではない人がメンションされた場合通知されるのが正しい挙動なのか確認(一部に挙動の不一致がありそう)。現状は通知されるためフィルタしない
// case 'followers': {
// 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));
// 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 +815,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);

View file

@ -91,13 +91,27 @@ export class RelayService {
return JSON.stringify(result);
}
@bindThis
private getAcceptedRelays(): Promise<MiRelay[]> {
return this.relaysCache.fetch(() => this.relaysRepository.findBy({
status: 'accepted',
}));
}
@bindThis
public async isRelayActor(actor: { inbox: string | null; sharedInbox: string | null }): Promise<boolean> {
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<void> {
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);

View file

@ -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

View file

@ -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<MiNote[]> {
if (!this.meilisearch || !this.meilisearchNoteIndex) {
throw new Error('MeiliSearch is not available');
throw new Error('Meilisearch is not available');
}
const filter: Q = {

View file

@ -164,4 +164,3 @@ export class SignupService {
return { account, secret };
}
}

View file

@ -259,7 +259,7 @@ export class ApInboxService {
@bindThis
private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise<string | void> {
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<string | void> {
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<string> {
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<string | void> {
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<string> {
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<string> {
if (actor.uri !== activity.actor) {
if (actor.uri !== getApId(activity.actor)) {
return 'skip: invalid actor';
}

View file

@ -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<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;

View file

@ -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,
};

View file

@ -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,

View file

@ -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;

View file

@ -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)),
};
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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)) {

View file

@ -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

View file

@ -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<typeof meta, typeof paramDef> { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
category: ps.category,
}, me);
return {
@ -94,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
description: created.description,
url: created.url,
roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration,
category: created.category,
};
});
}

View file

@ -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<typeof meta, typeof paramDef> { // eslint-
description: avatarDecoration.description,
url: avatarDecoration.url,
roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration,
category: avatarDecoration.category,
}));
});
}

View file

@ -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<typeof meta, typeof paramDef> { // eslint-
description: ps.description,
url: ps.url,
roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration,
category: ps.category,
}, me);
});
}

View file

@ -119,7 +119,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Update
this.driveFoldersRepository.update(folder.id, {
await this.driveFoldersRepository.update(folder.id, {
name: folder.name,
parentId: folder.parentId,
});

View file

@ -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<typeof import('../endpoints.js').default> | 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<typeof meta, typeof paramDef> { // 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 {

View file

@ -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<typeof import('../endpoints.js').default> | 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<typeof meta, typeof paramDef> { // eslint-
constructor(
) {
super(meta, paramDef, async () => {
const endpoints = await getEndpoints();
return endpoints.map(x => x.name);
});
}

View file

@ -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<typeof meta, typeof paramDef> { // eslint-
description: decoration.description,
url: decoration.url,
roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)),
category: decoration.category,
}));
});
}

View file

@ -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';

View file

@ -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';

View file

@ -3,16 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { genOpenapiSpec } from './gen-spec.js';
import { ApiDocPage } from './api-doc.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
@Injectable()
export class OpenApiServerService {
constructor(
@ -25,7 +23,8 @@ export class OpenApiServerService {
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/api-doc', async (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=86400');
return await reply.sendFile('/api-doc.html', staticAssets);
reply.type('text/html; charset=utf-8');
reply.send(await ApiDocPage());
});
fastify.get('/api.json', (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=600');

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function ApiDocPage() {
return (
<>
{'<!DOCTYPE html>'}
<html>
<head>
<meta charset="UTF-8" />
<title>Misskey API</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
{`body { margin: 0; padding: 0; }`}
</style>
</head>
<body>
<script id="api-reference" data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
</>
);
}

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -1,3 +1,4 @@
import { describe, test, beforeAll } from 'vitest';
import assert, { strictEqual } from 'node:assert';
import { createAccount, type LoginUser, sleep } from './utils.js';

View file

@ -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 }),

View file

@ -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';

View file

@ -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] });
});

View file

@ -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';

View file

@ -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
}

View file

@ -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;

View file

@ -25,8 +25,6 @@
"isolatedModules": true,
"jsx": "react-jsx",
"jsxImportSource": "@kitajs/html",
"rootDir": "../src",
"baseUrl": "./",
"paths": {
"@/*": ["../src/*"]
},

View file

@ -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;

View file

@ -6,6 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { describe, beforeAll, beforeEach, test } from 'vitest';
import {
api,
failedApiCall,

View file

@ -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';

View file

@ -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,

View file

@ -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';

View file

@ -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 } },

View file

@ -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';

View file

@ -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<string, unknown>) => {
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', () => {

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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', () => {

View file

@ -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';

View file

@ -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,

View file

@ -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';

Some files were not shown because too many files have changed in this diff Show more