Compare commits
	
		
			1 Commits
		
	
	
		
			chat
			...
			feat/bundl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f066247988 | 
							
								
								
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,33 +1,14 @@
 | 
			
		||||
## 2025.3.2
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
- Feat: チャットがリニューアルして復活しました
 | 
			
		||||
  - 既存のDM機能よりも便利で効率的になっています
 | 
			
		||||
  - チャットを受け付ける相手を制限できます
 | 
			
		||||
	- チャット機能を開放するかどうかをロールで制御できます
 | 
			
		||||
- Enhance: セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
 | 
			
		||||
- セキュリティを強化するため、ジョブキューのダッシュボード(bull-board)統合が削除されました。
 | 
			
		||||
  - Misskeyネイティブでダッシュボードを実装予定です
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Feat: 設定の管理が強化されました
 | 
			
		||||
  - 内部処理が一新され、安定性とパフォーマンスが向上しました
 | 
			
		||||
  - 全てのクライアント設定がエクスポート(バックアップ)/インポート対象に含まれるようになりました
 | 
			
		||||
    - プラグイン、テーマ、クライアントに追加されたすべてのアカウント情報も含まれるようになりました
 | 
			
		||||
  - 自動で設定データをサーバーにバックアップできるように
 | 
			
		||||
    - 設定→設定のプロファイル→自動バックアップ で有効にできます
 | 
			
		||||
    - 新しいデバイスからログインしたり、ブラウザから設定データが消えてしまったときに自動で復元されます(復元をスキップすることも可能)
 | 
			
		||||
  - 自動でバックアップされるように
 | 
			
		||||
  - 任意の設定項目をデバイス間で同期できるように
 | 
			
		||||
    - 設定項目の「...」メニュー→「デバイス間で同期」
 | 
			
		||||
    - 同期をオンにした際にサーバーに保存された値とローカルの値が競合する場合はどちらを優先するか選択できます
 | 
			
		||||
  - 任意の設定項目を初期値にリセットできるように
 | 
			
		||||
    - 設定項目の「...」メニュー→「初期値にリセット」
 | 
			
		||||
  - アカウントごとに設定値が分離される設定とそうでないクライアント設定が混在していた(かつ分離するかどうかを設定不可だった)のを、基本的に一律でクライアント全体に適用されるようにし、個別でアカウントごとに異なる設定を行えるように
 | 
			
		||||
    - 設定項目の「...」メニュー→「アカウントで上書き」をオンにすることで、設定値をそのアカウントでだけ適用するようにできます
 | 
			
		||||
  - ログアウトすると設定データもブラウザから消去されるようになりプライバシーが向上しました
 | 
			
		||||
    - 再度ログインすればサーバーのバックアップから設定データを復元可能です
 | 
			
		||||
  - エクスポートした設定データを他のサーバーでインポートして適用すること(設定の持ち運び)が可能になりました
 | 
			
		||||
- Feat: 画面を重ねて表示するオプションを実装(実験的)
 | 
			
		||||
	- 設定 → その他 → 実験的機能 → Enable stacking router view
 | 
			
		||||
- Enhance: プラグインの管理が強化されました
 | 
			
		||||
  - インストール/アンインストール/設定の変更時にリロード不要になりました
 | 
			
		||||
- Enhance: ログアウト時、ブラウザに保存されたWebクライアントのデータを全て消去するように
 | 
			
		||||
@@ -41,7 +22,6 @@
 | 
			
		||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Enhance 全体的なパフォーマンス向上
 | 
			
		||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
 | 
			
		||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
 | 
			
		||||
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										165
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										165
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1174,6 +1174,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * 保存しました
 | 
			
		||||
     */
 | 
			
		||||
    "saved": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * チャット
 | 
			
		||||
     */
 | 
			
		||||
    "messaging": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * アップロード
 | 
			
		||||
     */
 | 
			
		||||
@@ -1223,9 +1227,9 @@ export interface Locale extends ILocale {
 | 
			
		||||
     */
 | 
			
		||||
    "noMoreHistory": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * チャットを始める
 | 
			
		||||
     * チャットを開始
 | 
			
		||||
     */
 | 
			
		||||
    "startChat": string;
 | 
			
		||||
    "startMessaging": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * {n}人が読みました
 | 
			
		||||
     */
 | 
			
		||||
@@ -1978,6 +1982,14 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * クリップボードのテキストが長いです。テキストファイルとして添付しますか?
 | 
			
		||||
     */
 | 
			
		||||
    "attachAsFileQuestion": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * まだチャットはありません
 | 
			
		||||
     */
 | 
			
		||||
    "noMessagesYet": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 新しいメッセージがあります
 | 
			
		||||
     */
 | 
			
		||||
    "newMessageExists": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * メッセージに添付できるファイルはひとつです
 | 
			
		||||
     */
 | 
			
		||||
@@ -5346,143 +5358,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * 情報
 | 
			
		||||
     */
 | 
			
		||||
    "information": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * チャット
 | 
			
		||||
     */
 | 
			
		||||
    "chat": string;
 | 
			
		||||
    "_chat": {
 | 
			
		||||
        /**
 | 
			
		||||
         * まだメッセージはありません
 | 
			
		||||
         */
 | 
			
		||||
        "noMessagesYet": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 新しいメッセージ
 | 
			
		||||
         */
 | 
			
		||||
        "newMessage": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 個人チャット
 | 
			
		||||
         */
 | 
			
		||||
        "individualChat": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 特定ユーザーとの一対一のチャットができます。
 | 
			
		||||
         */
 | 
			
		||||
        "individualChat_description": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ルームチャット
 | 
			
		||||
         */
 | 
			
		||||
        "roomChat": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 複数人でのチャットができます。
 | 
			
		||||
         * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。
 | 
			
		||||
         */
 | 
			
		||||
        "roomChat_description": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ルームを作成
 | 
			
		||||
         */
 | 
			
		||||
        "createRoom": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ユーザーを招待してチャットを始めましょう
 | 
			
		||||
         */
 | 
			
		||||
        "inviteUserToChat": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 作成したルーム
 | 
			
		||||
         */
 | 
			
		||||
        "yourRooms": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 参加中のルーム
 | 
			
		||||
         */
 | 
			
		||||
        "joiningRooms": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 招待
 | 
			
		||||
         */
 | 
			
		||||
        "invitations": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 招待はありません
 | 
			
		||||
         */
 | 
			
		||||
        "noInvitations": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 履歴
 | 
			
		||||
         */
 | 
			
		||||
        "history": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 履歴はありません
 | 
			
		||||
         */
 | 
			
		||||
        "noHistory": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ルームはありません
 | 
			
		||||
         */
 | 
			
		||||
        "noRooms": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * ユーザーを招待
 | 
			
		||||
         */
 | 
			
		||||
        "inviteUser": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 参加
 | 
			
		||||
         */
 | 
			
		||||
        "join": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 無視
 | 
			
		||||
         */
 | 
			
		||||
        "ignore": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このユーザーとのチャットを開始できません
 | 
			
		||||
         */
 | 
			
		||||
        "cannotChatWithTheUser": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * チャットが使えない状態になっているか、相手がチャットを開放していません。
 | 
			
		||||
         */
 | 
			
		||||
        "cannotChatWithTheUser_description": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * チャットする
 | 
			
		||||
         */
 | 
			
		||||
        "chatWithThisUser": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このユーザーはフォロワーからのみチャットを受け付けています。
 | 
			
		||||
         */
 | 
			
		||||
        "thisUserAllowsChatOnlyFromFollowers": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このユーザーはフォローしているユーザーからのみチャットを受け付けています。
 | 
			
		||||
         */
 | 
			
		||||
        "thisUserAllowsChatOnlyFromFollowing": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このユーザーは相互フォローのユーザーからのみチャットを受け付けています。
 | 
			
		||||
         */
 | 
			
		||||
        "thisUserAllowsChatOnlyFromMutualFollowing": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このユーザーは誰からもチャットを受け付けていません。
 | 
			
		||||
         */
 | 
			
		||||
        "thisUserNotAllowedChatAnyone": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * チャットを許可する相手
 | 
			
		||||
         */
 | 
			
		||||
        "chatAllowedUsers": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。
 | 
			
		||||
         */
 | 
			
		||||
        "chatAllowedUsers_note": string;
 | 
			
		||||
        "_chatAllowedUsers": {
 | 
			
		||||
            /**
 | 
			
		||||
             * 誰でも
 | 
			
		||||
             */
 | 
			
		||||
            "everyone": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 自分のフォロワーのみ
 | 
			
		||||
             */
 | 
			
		||||
            "followers": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 自分がフォローしているユーザーのみ
 | 
			
		||||
             */
 | 
			
		||||
            "following": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 相互フォローのユーザーのみ
 | 
			
		||||
             */
 | 
			
		||||
            "mutual": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * 誰も許可しない
 | 
			
		||||
             */
 | 
			
		||||
            "none": string;
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    "_emojiPalette": {
 | 
			
		||||
        /**
 | 
			
		||||
         * パレット
 | 
			
		||||
@@ -7410,10 +7285,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
             * リストのインポートを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canImportUserLists": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * チャットを許可
 | 
			
		||||
             */
 | 
			
		||||
            "canChat": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_condition": {
 | 
			
		||||
            /**
 | 
			
		||||
@@ -8855,14 +8726,6 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 違反を報告する
 | 
			
		||||
         */
 | 
			
		||||
        "write:report-abuse": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * チャットを操作する
 | 
			
		||||
         */
 | 
			
		||||
        "write:chat": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * チャットを閲覧する
 | 
			
		||||
         */
 | 
			
		||||
        "read:chat": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_auth": {
 | 
			
		||||
        /**
 | 
			
		||||
 
 | 
			
		||||
@@ -289,6 +289,7 @@ deleteAreYouSure: "「{x}」を削除しますか?"
 | 
			
		||||
resetAreYouSure: "リセットしますか?"
 | 
			
		||||
areYouSure: "よろしいですか?"
 | 
			
		||||
saved: "保存しました"
 | 
			
		||||
messaging: "チャット"
 | 
			
		||||
upload: "アップロード"
 | 
			
		||||
keepOriginalUploading: "オリジナル画像を保持"
 | 
			
		||||
keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。"
 | 
			
		||||
@@ -301,7 +302,7 @@ uploadFromUrlMayTakeTime: "アップロードが完了するまで時間がか
 | 
			
		||||
explore: "みつける"
 | 
			
		||||
messageRead: "既読"
 | 
			
		||||
noMoreHistory: "これより過去の履歴はありません"
 | 
			
		||||
startChat: "チャットを始める"
 | 
			
		||||
startMessaging: "チャットを開始"
 | 
			
		||||
nUsersRead: "{n}人が読みました"
 | 
			
		||||
agreeTo: "{0}に同意"
 | 
			
		||||
agree: "同意する"
 | 
			
		||||
@@ -490,6 +491,8 @@ noteOf: "{user}のノート"
 | 
			
		||||
quoteAttached: "引用付き"
 | 
			
		||||
quoteQuestion: "引用として添付しますか?"
 | 
			
		||||
attachAsFileQuestion: "クリップボードのテキストが長いです。テキストファイルとして添付しますか?"
 | 
			
		||||
noMessagesYet: "まだチャットはありません"
 | 
			
		||||
newMessageExists: "新しいメッセージがあります"
 | 
			
		||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
 | 
			
		||||
signinRequired: "続行する前に、登録またはログインが必要です"
 | 
			
		||||
signinOrContinueOnRemote: "続行するには、お使いのサーバーに移動するか、このサーバーに登録・ログインする必要があります"
 | 
			
		||||
@@ -1332,42 +1335,6 @@ emojiPalette: "絵文字パレット"
 | 
			
		||||
postForm: "投稿フォーム"
 | 
			
		||||
textCount: "文字数"
 | 
			
		||||
information: "情報"
 | 
			
		||||
chat: "チャット"
 | 
			
		||||
 | 
			
		||||
_chat:
 | 
			
		||||
  noMessagesYet: "まだメッセージはありません"
 | 
			
		||||
  newMessage: "新しいメッセージ"
 | 
			
		||||
  individualChat: "個人チャット"
 | 
			
		||||
  individualChat_description: "特定ユーザーとの一対一のチャットができます。"
 | 
			
		||||
  roomChat: "ルームチャット"
 | 
			
		||||
  roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
 | 
			
		||||
  createRoom: "ルームを作成"
 | 
			
		||||
  inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
 | 
			
		||||
  yourRooms: "作成したルーム"
 | 
			
		||||
  joiningRooms: "参加中のルーム"
 | 
			
		||||
  invitations: "招待"
 | 
			
		||||
  noInvitations: "招待はありません"
 | 
			
		||||
  history: "履歴"
 | 
			
		||||
  noHistory: "履歴はありません"
 | 
			
		||||
  noRooms: "ルームはありません"
 | 
			
		||||
  inviteUser: "ユーザーを招待"
 | 
			
		||||
  join: "参加"
 | 
			
		||||
  ignore: "無視"
 | 
			
		||||
  cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
 | 
			
		||||
  cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
 | 
			
		||||
  chatWithThisUser: "チャットする"
 | 
			
		||||
  thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
 | 
			
		||||
  thisUserAllowsChatOnlyFromFollowing: "このユーザーはフォローしているユーザーからのみチャットを受け付けています。"
 | 
			
		||||
  thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
 | 
			
		||||
  thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
 | 
			
		||||
  chatAllowedUsers: "チャットを許可する相手"
 | 
			
		||||
  chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
 | 
			
		||||
  _chatAllowedUsers:
 | 
			
		||||
    everyone: "誰でも"
 | 
			
		||||
    followers: "自分のフォロワーのみ"
 | 
			
		||||
    following: "自分がフォローしているユーザーのみ"
 | 
			
		||||
    mutual: "相互フォローのユーザーのみ"
 | 
			
		||||
    none: "誰も許可しない"
 | 
			
		||||
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "パレット"
 | 
			
		||||
@@ -1919,7 +1886,6 @@ _role:
 | 
			
		||||
    canImportFollowing: "フォローのインポートを許可"
 | 
			
		||||
    canImportMuting: "ミュートのインポートを許可"
 | 
			
		||||
    canImportUserLists: "リストのインポートを許可"
 | 
			
		||||
    canChat: "チャットを許可"
 | 
			
		||||
  _condition:
 | 
			
		||||
    roleAssignedTo: "マニュアルロールにアサイン済み"
 | 
			
		||||
    isLocal: "ローカルユーザー"
 | 
			
		||||
@@ -2325,8 +2291,6 @@ _permissions:
 | 
			
		||||
  "read:clip-favorite": "クリップのいいねを見る"
 | 
			
		||||
  "read:federation": "連合に関する情報を取得する"
 | 
			
		||||
  "write:report-abuse": "違反を報告する"
 | 
			
		||||
  "write:chat": "チャットを操作する"
 | 
			
		||||
  "read:chat": "チャットを閲覧する"
 | 
			
		||||
 | 
			
		||||
_auth:
 | 
			
		||||
  shareAccessTitle: "アプリへのアクセス許可"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2025.3.2-beta.9",
 | 
			
		||||
	"version": "2025.3.2-beta.6",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class Chat1742203321812 {
 | 
			
		||||
    name = 'Chat1742203321812'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "chat_room" ("id" character varying(32) NOT NULL, "name" character varying(256) NOT NULL, "ownerId" character varying(32) NOT NULL, CONSTRAINT "PK_8aa3a52cf74c96469f0ef9fbe3e" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_f0d8ad64243fa2ca2800da0dfd" ON "chat_room" ("ownerId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "chat_message" ("id" character varying(32) NOT NULL, "fromUserId" character varying(32) NOT NULL, "toUserId" character varying(32), "toRoomId" character varying(32), "text" character varying(4096), "uri" character varying(512), "reads" character varying(32) array NOT NULL DEFAULT '{}', "fileId" character varying(32), "reactions" character varying(1024) array NOT NULL DEFAULT '{}', CONSTRAINT "PK_3cc0d85193aade457d3077dd06b" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_79a26e7a4d9afa5e4fc05f134e" ON "chat_message" ("fromUserId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_25e097b51d7622c249452c6f75" ON "chat_message" ("toUserId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_f006b8a76efd1abf9f221c175c" ON "chat_message" ("toRoomId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "chat_room_membership" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_2bd59c741e571b283c048beb69a" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_d99c5279460fb77ef58c596ce5" ON "chat_room_membership" ("userId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_c25143ebab714e930aeca1c0e8" ON "chat_room_membership" ("roomId") `);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room" ADD CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed" FOREIGN KEY ("fromUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_25e097b51d7622c249452c6f757" FOREIGN KEY ("toUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce" FOREIGN KEY ("toRoomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" ADD CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_d99c5279460fb77ef58c596ce51" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_membership" ADD CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_c25143ebab714e930aeca1c0e8d"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_membership" DROP CONSTRAINT "FK_d99c5279460fb77ef58c596ce51"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_fd0f9a4879430239715ad4f8e2a"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_f006b8a76efd1abf9f221c175ce"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_25e097b51d7622c249452c6f757"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_message" DROP CONSTRAINT "FK_79a26e7a4d9afa5e4fc05f134ed"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room" DROP CONSTRAINT "FK_f0d8ad64243fa2ca2800da0dfd6"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_c25143ebab714e930aeca1c0e8"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_d99c5279460fb77ef58c596ce5"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "chat_room_membership"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_f006b8a76efd1abf9f221c175c"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_25e097b51d7622c249452c6f75"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_79a26e7a4d9afa5e4fc05f134e"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "chat_message"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_f0d8ad64243fa2ca2800da0dfd"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "chat_room"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class Chat21742608337548 {
 | 
			
		||||
    name = 'Chat21742608337548'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "user" ADD "chatScope" character varying(128) NOT NULL DEFAULT 'mutual'`);
 | 
			
		||||
        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_185b6b5afa707b5d36d1ce3144" ON "chat_room_membership" ("userId", "roomId") `);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_185b6b5afa707b5d36d1ce3144"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "chatScope"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class Chat31742617546147 {
 | 
			
		||||
    name = 'Chat31742617546147'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "chat_approval" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "otherId" character varying(32) NOT NULL, CONSTRAINT "PK_fbbb95d60acf5c85388345b5f5d" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_530257863e1381a7f2f1d3282f" ON "chat_approval" ("userId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_b1d46037f23d170da5c05fdf75" ON "chat_approval" ("otherId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_12c4768a2f706fc267f2078903" ON "chat_approval" ("userId", "otherId") `);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_530257863e1381a7f2f1d3282fe" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_approval" ADD CONSTRAINT "FK_b1d46037f23d170da5c05fdf755" FOREIGN KEY ("otherId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_b1d46037f23d170da5c05fdf755"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_approval" DROP CONSTRAINT "FK_530257863e1381a7f2f1d3282fe"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_12c4768a2f706fc267f2078903"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_b1d46037f23d170da5c05fdf75"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_530257863e1381a7f2f1d3282f"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "chat_approval"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class Chat41742707840715 {
 | 
			
		||||
    name = 'Chat41742707840715'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE TABLE "chat_room_invitation" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "roomId" character varying(32) NOT NULL, CONSTRAINT "PK_9d489521a312dd28225672de2dc" PRIMARY KEY ("id"))`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_8552bb38e7ed038c5bdd398a38" ON "chat_room_invitation" ("userId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_5f265075b215fc390a57523b12" ON "chat_room_invitation" ("roomId") `);
 | 
			
		||||
        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_044f2a7962b8ee5bbfaa02e8a3" ON "chat_room_invitation" ("userId", "roomId") `);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD CONSTRAINT "FK_5f265075b215fc390a57523b12a" FOREIGN KEY ("roomId") REFERENCES "chat_room"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_5f265075b215fc390a57523b12a"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP CONSTRAINT "FK_8552bb38e7ed038c5bdd398a384"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_044f2a7962b8ee5bbfaa02e8a3"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_5f265075b215fc390a57523b12"`);
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_8552bb38e7ed038c5bdd398a38"`);
 | 
			
		||||
        await queryRunner.query(`DROP TABLE "chat_room_invitation"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class Chat51742721896936 {
 | 
			
		||||
    name = 'Chat51742721896936'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" ADD "ignored" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "chat_room_invitation" DROP COLUMN "ignored"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -37,17 +37,17 @@
 | 
			
		||||
	},
 | 
			
		||||
	"optionalDependencies": {
 | 
			
		||||
		"@swc/core-android-arm64": "1.3.11",
 | 
			
		||||
		"@swc/core-darwin-arm64": "1.11.11",
 | 
			
		||||
		"@swc/core-darwin-x64": "1.11.11",
 | 
			
		||||
		"@swc/core-darwin-arm64": "1.10.16",
 | 
			
		||||
		"@swc/core-darwin-x64": "1.10.16",
 | 
			
		||||
		"@swc/core-freebsd-x64": "1.3.11",
 | 
			
		||||
		"@swc/core-linux-arm-gnueabihf": "1.11.11",
 | 
			
		||||
		"@swc/core-linux-arm64-gnu": "1.11.11",
 | 
			
		||||
		"@swc/core-linux-arm64-musl": "1.11.11",
 | 
			
		||||
		"@swc/core-linux-x64-gnu": "1.11.11",
 | 
			
		||||
		"@swc/core-linux-x64-musl": "1.11.11",
 | 
			
		||||
		"@swc/core-win32-arm64-msvc": "1.11.11",
 | 
			
		||||
		"@swc/core-win32-ia32-msvc": "1.11.11",
 | 
			
		||||
		"@swc/core-win32-x64-msvc": "1.11.11",
 | 
			
		||||
		"@swc/core-linux-arm-gnueabihf": "1.10.16",
 | 
			
		||||
		"@swc/core-linux-arm64-gnu": "1.10.16",
 | 
			
		||||
		"@swc/core-linux-arm64-musl": "1.10.16",
 | 
			
		||||
		"@swc/core-linux-x64-gnu": "1.10.16",
 | 
			
		||||
		"@swc/core-linux-x64-musl": "1.10.16",
 | 
			
		||||
		"@swc/core-win32-arm64-msvc": "1.10.16",
 | 
			
		||||
		"@swc/core-win32-ia32-msvc": "1.10.16",
 | 
			
		||||
		"@swc/core-win32-x64-msvc": "1.10.16",
 | 
			
		||||
		"@tensorflow/tfjs": "4.22.0",
 | 
			
		||||
		"@tensorflow/tfjs-node": "4.22.0",
 | 
			
		||||
		"bufferutil": "4.0.9",
 | 
			
		||||
@@ -67,23 +67,23 @@
 | 
			
		||||
		"utf-8-validate": "6.0.5"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@aws-sdk/client-s3": "3.772.0",
 | 
			
		||||
		"@aws-sdk/lib-storage": "3.772.0",
 | 
			
		||||
		"@aws-sdk/client-s3": "3.749.0",
 | 
			
		||||
		"@aws-sdk/lib-storage": "3.749.0",
 | 
			
		||||
		"@discordapp/twemoji": "15.1.0",
 | 
			
		||||
		"@fastify/accepts": "5.0.2",
 | 
			
		||||
		"@fastify/cookie": "11.0.2",
 | 
			
		||||
		"@fastify/cors": "10.1.0",
 | 
			
		||||
		"@fastify/cors": "10.0.2",
 | 
			
		||||
		"@fastify/express": "4.0.2",
 | 
			
		||||
		"@fastify/http-proxy": "10.0.2",
 | 
			
		||||
		"@fastify/multipart": "9.0.3",
 | 
			
		||||
		"@fastify/static": "8.1.1",
 | 
			
		||||
		"@fastify/static": "8.1.0",
 | 
			
		||||
		"@fastify/view": "10.0.2",
 | 
			
		||||
		"@misskey-dev/sharp-read-bmp": "1.2.0",
 | 
			
		||||
		"@misskey-dev/summaly": "5.2.0",
 | 
			
		||||
		"@napi-rs/canvas": "0.1.68",
 | 
			
		||||
		"@nestjs/common": "11.0.12",
 | 
			
		||||
		"@nestjs/core": "11.0.12",
 | 
			
		||||
		"@nestjs/testing": "11.0.12",
 | 
			
		||||
		"@napi-rs/canvas": "0.1.67",
 | 
			
		||||
		"@nestjs/common": "11.0.9",
 | 
			
		||||
		"@nestjs/core": "11.0.9",
 | 
			
		||||
		"@nestjs/testing": "11.0.9",
 | 
			
		||||
		"@peertube/http-signature": "1.7.0",
 | 
			
		||||
		"@sentry/node": "8.55.0",
 | 
			
		||||
		"@sentry/profiling-node": "8.55.0",
 | 
			
		||||
@@ -91,7 +91,7 @@
 | 
			
		||||
		"@sinonjs/fake-timers": "11.3.1",
 | 
			
		||||
		"@smithy/node-http-handler": "2.5.0",
 | 
			
		||||
		"@swc/cli": "0.6.0",
 | 
			
		||||
		"@swc/core": "1.11.11",
 | 
			
		||||
		"@swc/core": "1.10.16",
 | 
			
		||||
		"@twemoji/parser": "15.1.1",
 | 
			
		||||
		"accepts": "1.3.8",
 | 
			
		||||
		"ajv": "8.17.1",
 | 
			
		||||
@@ -100,7 +100,7 @@
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"blurhash": "2.0.5",
 | 
			
		||||
		"body-parser": "1.20.3",
 | 
			
		||||
		"bullmq": "5.44.1",
 | 
			
		||||
		"bullmq": "5.41.1",
 | 
			
		||||
		"cacheable-lookup": "7.0.0",
 | 
			
		||||
		"cbor": "9.0.2",
 | 
			
		||||
		"chalk": "5.4.1",
 | 
			
		||||
@@ -122,7 +122,7 @@
 | 
			
		||||
		"hpagent": "1.2.0",
 | 
			
		||||
		"htmlescape": "1.1.1",
 | 
			
		||||
		"http-link-header": "1.1.3",
 | 
			
		||||
		"ioredis": "5.6.0",
 | 
			
		||||
		"ioredis": "5.5.0",
 | 
			
		||||
		"ip-cidr": "4.0.2",
 | 
			
		||||
		"ipaddr.js": "2.2.0",
 | 
			
		||||
		"is-svg": "5.1.0",
 | 
			
		||||
@@ -131,26 +131,26 @@
 | 
			
		||||
		"json5": "2.2.3",
 | 
			
		||||
		"jsonld": "8.3.3",
 | 
			
		||||
		"jsrsasign": "11.1.0",
 | 
			
		||||
		"juice": "11.0.1",
 | 
			
		||||
		"meilisearch": "0.49.0",
 | 
			
		||||
		"juice": "11.0.0",
 | 
			
		||||
		"meilisearch": "0.48.2",
 | 
			
		||||
		"mfm-js": "0.24.0",
 | 
			
		||||
		"microformats-parser": "2.0.2",
 | 
			
		||||
		"mime-types": "2.1.35",
 | 
			
		||||
		"misskey-js": "workspace:*",
 | 
			
		||||
		"misskey-reversi": "workspace:*",
 | 
			
		||||
		"ms": "3.0.0-canary.1",
 | 
			
		||||
		"nanoid": "5.1.5",
 | 
			
		||||
		"nanoid": "5.1.0",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
		"node-fetch": "3.3.2",
 | 
			
		||||
		"nodemailer": "6.10.0",
 | 
			
		||||
		"nsfwjs": "4.2.0",
 | 
			
		||||
		"oauth": "0.10.2",
 | 
			
		||||
		"oauth": "0.10.0",
 | 
			
		||||
		"oauth2orize": "1.12.0",
 | 
			
		||||
		"oauth2orize-pkce": "0.1.2",
 | 
			
		||||
		"os-utils": "0.0.14",
 | 
			
		||||
		"otpauth": "9.3.6",
 | 
			
		||||
		"parse5": "7.2.1",
 | 
			
		||||
		"pg": "8.14.1",
 | 
			
		||||
		"pg": "8.13.3",
 | 
			
		||||
		"pkce-challenge": "4.1.0",
 | 
			
		||||
		"probe-image-size": "7.2.3",
 | 
			
		||||
		"promise-limit": "2.7.0",
 | 
			
		||||
@@ -163,8 +163,8 @@
 | 
			
		||||
		"reflect-metadata": "0.2.2",
 | 
			
		||||
		"rename": "1.0.4",
 | 
			
		||||
		"rss-parser": "3.13.0",
 | 
			
		||||
		"rxjs": "7.8.2",
 | 
			
		||||
		"sanitize-html": "2.15.0",
 | 
			
		||||
		"rxjs": "7.8.1",
 | 
			
		||||
		"sanitize-html": "2.14.0",
 | 
			
		||||
		"secure-json-parse": "3.0.2",
 | 
			
		||||
		"sharp": "0.33.5",
 | 
			
		||||
		"slacc": "0.0.10",
 | 
			
		||||
@@ -173,14 +173,14 @@
 | 
			
		||||
		"systeminformation": "5.25.11",
 | 
			
		||||
		"tinycolor2": "1.6.0",
 | 
			
		||||
		"tmp": "0.2.3",
 | 
			
		||||
		"tsc-alias": "1.8.11",
 | 
			
		||||
		"tsc-alias": "1.8.10",
 | 
			
		||||
		"tsconfig-paths": "4.2.0",
 | 
			
		||||
		"typeorm": "0.3.21",
 | 
			
		||||
		"typescript": "5.8.2",
 | 
			
		||||
		"ulid": "2.4.0",
 | 
			
		||||
		"typeorm": "0.3.20",
 | 
			
		||||
		"typescript": "5.7.3",
 | 
			
		||||
		"ulid": "2.3.0",
 | 
			
		||||
		"vary": "1.1.2",
 | 
			
		||||
		"web-push": "3.6.7",
 | 
			
		||||
		"ws": "8.18.1",
 | 
			
		||||
		"ws": "8.18.0",
 | 
			
		||||
		"xev": "3.0.2"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
@@ -204,7 +204,7 @@
 | 
			
		||||
		"@types/jsrsasign": "10.5.15",
 | 
			
		||||
		"@types/mime-types": "2.1.4",
 | 
			
		||||
		"@types/ms": "0.7.34",
 | 
			
		||||
		"@types/node": "22.13.10",
 | 
			
		||||
		"@types/node": "22.13.4",
 | 
			
		||||
		"@types/nodemailer": "6.4.17",
 | 
			
		||||
		"@types/oauth": "0.9.6",
 | 
			
		||||
		"@types/oauth2orize": "1.11.5",
 | 
			
		||||
@@ -223,9 +223,9 @@
 | 
			
		||||
		"@types/tmp": "0.2.6",
 | 
			
		||||
		"@types/vary": "1.1.3",
 | 
			
		||||
		"@types/web-push": "3.6.4",
 | 
			
		||||
		"@types/ws": "8.18.0",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.27.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.27.0",
 | 
			
		||||
		"@types/ws": "8.5.14",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.24.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.24.0",
 | 
			
		||||
		"aws-sdk-client-mock": "4.1.0",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"eslint-plugin-import": "2.31.0",
 | 
			
		||||
@@ -235,6 +235,7 @@
 | 
			
		||||
		"jest-mock": "29.7.0",
 | 
			
		||||
		"nodemon": "3.1.9",
 | 
			
		||||
		"pid-port": "1.0.2",
 | 
			
		||||
		"simple-oauth2": "5.1.0"
 | 
			
		||||
		"simple-oauth2": "5.1.0",
 | 
			
		||||
		"vite": "6.2.1"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,13 @@
 | 
			
		||||
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname, resolve } from 'node:path';
 | 
			
		||||
import { dirname, join, resolve } from 'node:path';
 | 
			
		||||
import * as yaml from 'js-yaml';
 | 
			
		||||
import * as Sentry from '@sentry/node';
 | 
			
		||||
import locale from '../../../locales/index.js';
 | 
			
		||||
import type { RedisOptions } from 'ioredis';
 | 
			
		||||
import type { Manifest, ManifestChunk } from 'vite';
 | 
			
		||||
import type { ILocale } from '../../../locales/index.js';
 | 
			
		||||
 | 
			
		||||
type RedisOptionsSource = Partial<RedisOptions> & {
 | 
			
		||||
	host: string;
 | 
			
		||||
@@ -185,9 +188,12 @@ export type Config = {
 | 
			
		||||
	authUrl: string;
 | 
			
		||||
	driveUrl: string;
 | 
			
		||||
	userAgent: string;
 | 
			
		||||
	frontendEntry: string;
 | 
			
		||||
	localeEntries: Record<string, string>;
 | 
			
		||||
	errorLocaleMessages: Record<string, ILocale>;
 | 
			
		||||
	configEntry: ManifestChunk;
 | 
			
		||||
	frontendEntry: ManifestChunk;
 | 
			
		||||
	frontendManifestExists: boolean;
 | 
			
		||||
	frontendEmbedEntry: string;
 | 
			
		||||
	frontendEmbedEntry: ManifestChunk;
 | 
			
		||||
	frontendEmbedManifestExists: boolean;
 | 
			
		||||
	mediaProxy: string;
 | 
			
		||||
	externalMediaProxyEnabled: boolean;
 | 
			
		||||
@@ -229,12 +235,23 @@ export function loadConfig(): Config {
 | 
			
		||||
 | 
			
		||||
	const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
 | 
			
		||||
	const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
 | 
			
		||||
	const frontendManifest = frontendManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 | 
			
		||||
	const frontendEmbedManifest = frontendEmbedManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/boot.ts': { file: 'src/boot.ts' } };
 | 
			
		||||
	const frontendManifest: Manifest = frontendManifestExists
 | 
			
		||||
		? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: Object.entries(locale).reduce<Record<string, ManifestChunk>>((a, [k]) => {
 | 
			
		||||
			a[`locale:${k}`] = { file: `locale:${k}` };
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {
 | 
			
		||||
			'src/_boot_.ts': { file: 'src/_boot_.ts' },
 | 
			
		||||
			'../frontend-shared/js/config.ts': { file: join('@fs', _dirname.slice(1), '../../frontend-shared/js/config.ts') },
 | 
			
		||||
		});
 | 
			
		||||
	const frontendEmbedManifest: Manifest = frontendEmbedManifestExists
 | 
			
		||||
		? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: Object.entries(locale).reduce<Record<string, ManifestChunk>>((a, [k]) => {
 | 
			
		||||
			a[`locale:${k}`] = { file: `locale:${k}` };
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {
 | 
			
		||||
			'src/boot.ts': { file: 'src/boot.ts' },
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 | 
			
		||||
 | 
			
		||||
@@ -310,6 +327,20 @@ export function loadConfig(): Config {
 | 
			
		||||
			config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
 | 
			
		||||
			: null,
 | 
			
		||||
		userAgent: `Misskey/${version} (${config.url})`,
 | 
			
		||||
		localeEntries: Object.entries<ManifestChunk>(frontendManifest).reduce<Record<string, string>>((a, [k, v]) => {
 | 
			
		||||
			if (k.startsWith('locale:')) {
 | 
			
		||||
				a[k.slice('locale:'.length)] = v.file;
 | 
			
		||||
			}
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {}),
 | 
			
		||||
		errorLocaleMessages: Object.entries(locale).reduce<Record<string, ILocale>>((a, [k, v]) => {
 | 
			
		||||
			a[k] = {
 | 
			
		||||
				_bootErrors: v._bootErrors,
 | 
			
		||||
				reload: v.reload,
 | 
			
		||||
			};
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {}),
 | 
			
		||||
		configEntry: frontendManifest['../frontend-shared/js/config.ts'],
 | 
			
		||||
		frontendEntry: frontendManifest['src/_boot_.ts'],
 | 
			
		||||
		frontendManifestExists: frontendManifestExists,
 | 
			
		||||
		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,602 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { Brackets } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { ChatApprovalsRepository, ChatMessagesRepository, ChatRoomInvitationsRepository, ChatRoomMembershipsRepository, ChatRoomsRepository, MiChatMessage, MiChatRoom, MiChatRoomMembership, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
 | 
			
		||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
 | 
			
		||||
import { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
 | 
			
		||||
const MAX_ROOM_MEMBERS = 30;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ChatService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.config)
 | 
			
		||||
		private config: Config,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatMessagesRepository)
 | 
			
		||||
		private chatMessagesRepository: ChatMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatApprovalsRepository)
 | 
			
		||||
		private chatApprovalsRepository: ChatApprovalsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomsRepository)
 | 
			
		||||
		private chatRoomsRepository: ChatRoomsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomInvitationsRepository)
 | 
			
		||||
		private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomMembershipsRepository)
 | 
			
		||||
		private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.mutingsRepository)
 | 
			
		||||
		private mutingsRepository: MutingsRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
		private apRendererService: ApRendererService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private pushNotificationService: PushNotificationService,
 | 
			
		||||
		private userBlockingService: UserBlockingService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private userFollowingService: UserFollowingService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
 | 
			
		||||
		text?: string | null;
 | 
			
		||||
		file?: MiDriveFile | null;
 | 
			
		||||
		uri?: string | null;
 | 
			
		||||
	}): Promise<Packed<'ChatMessageLite'>> {
 | 
			
		||||
		if (fromUser.id === toUser.id) {
 | 
			
		||||
			throw new Error('yourself');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const approvals = await this.chatApprovalsRepository.createQueryBuilder('approval')
 | 
			
		||||
			.where(new Brackets(qb => { // 自分が相手を許可しているか
 | 
			
		||||
				qb.where('approval.userId = :fromUserId', { fromUserId: fromUser.id })
 | 
			
		||||
					.andWhere('approval.otherId = :toUserId', { toUserId: toUser.id });
 | 
			
		||||
			}))
 | 
			
		||||
			.orWhere(new Brackets(qb => { // 相手が自分を許可しているか
 | 
			
		||||
				qb.where('approval.userId = :toUserId', { toUserId: toUser.id })
 | 
			
		||||
					.andWhere('approval.otherId = :fromUserId', { fromUserId: fromUser.id });
 | 
			
		||||
			}))
 | 
			
		||||
			.take(2)
 | 
			
		||||
			.getMany();
 | 
			
		||||
 | 
			
		||||
		const otherApprovedMe = approvals.some(approval => approval.userId === toUser.id);
 | 
			
		||||
		const iApprovedOther = approvals.some(approval => approval.userId === fromUser.id);
 | 
			
		||||
 | 
			
		||||
		if (!otherApprovedMe) {
 | 
			
		||||
			if (toUser.chatScope === 'none') {
 | 
			
		||||
				throw new Error('recipient is cannot chat (none)');
 | 
			
		||||
			} else if (toUser.chatScope === 'followers') {
 | 
			
		||||
				const isFollower = await this.userFollowingService.isFollowing(fromUser.id, toUser.id);
 | 
			
		||||
				if (!isFollower) {
 | 
			
		||||
					throw new Error('recipient is cannot chat (followers)');
 | 
			
		||||
				}
 | 
			
		||||
			} else if (toUser.chatScope === 'following') {
 | 
			
		||||
				const isFollowing = await this.userFollowingService.isFollowing(toUser.id, fromUser.id);
 | 
			
		||||
				if (!isFollowing) {
 | 
			
		||||
					throw new Error('recipient is cannot chat (following)');
 | 
			
		||||
				}
 | 
			
		||||
			} else if (toUser.chatScope === 'mutual') {
 | 
			
		||||
				const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
 | 
			
		||||
				if (!isMutual) {
 | 
			
		||||
					throw new Error('recipient is cannot chat (mutual)');
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
 | 
			
		||||
			throw new Error('recipient is cannot chat (policy)');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
 | 
			
		||||
		if (blocked) {
 | 
			
		||||
			throw new Error('blocked');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const message = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			fromUserId: fromUser.id,
 | 
			
		||||
			toUserId: toUser.id,
 | 
			
		||||
			text: params.text ? params.text.trim() : null,
 | 
			
		||||
			fileId: params.file ? params.file.id : null,
 | 
			
		||||
			reads: [],
 | 
			
		||||
			uri: params.uri ?? null,
 | 
			
		||||
		} satisfies Partial<MiChatMessage>;
 | 
			
		||||
 | 
			
		||||
		const inserted = await this.chatMessagesRepository.insertOne(message);
 | 
			
		||||
 | 
			
		||||
		// 相手を許可しておく
 | 
			
		||||
		if (!iApprovedOther) {
 | 
			
		||||
			this.chatApprovalsRepository.insertOne({
 | 
			
		||||
				id: this.idService.gen(),
 | 
			
		||||
				userId: fromUser.id,
 | 
			
		||||
				otherId: toUser.id,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const packedMessage = await this.chatEntityService.packMessageLite(inserted);
 | 
			
		||||
 | 
			
		||||
		if (this.userEntityService.isLocalUser(toUser)) {
 | 
			
		||||
			const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
			redisPipeline.set(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`, message.id);
 | 
			
		||||
			redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`);
 | 
			
		||||
			redisPipeline.exec();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.userEntityService.isLocalUser(fromUser)) {
 | 
			
		||||
			// 自分のストリーム
 | 
			
		||||
			this.globalEventService.publishChatUserStream(fromUser.id, toUser.id, 'message', packedMessage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.userEntityService.isLocalUser(toUser)) {
 | 
			
		||||
			// 相手のストリーム
 | 
			
		||||
			this.globalEventService.publishChatUserStream(toUser.id, fromUser.id, 'message', packedMessage);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 3秒経っても既読にならなかったらイベント発行
 | 
			
		||||
		if (this.userEntityService.isLocalUser(toUser)) {
 | 
			
		||||
			setTimeout(async () => {
 | 
			
		||||
				const marker = await this.redisClient.get(`newUserChatMessageExists:${toUser.id}:${fromUser.id}`);
 | 
			
		||||
 | 
			
		||||
				if (marker == null) return; // 既読
 | 
			
		||||
 | 
			
		||||
				const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted, toUser);
 | 
			
		||||
				this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
 | 
			
		||||
				this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
 | 
			
		||||
			}, 3000);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return packedMessage;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: {
 | 
			
		||||
		text?: string | null;
 | 
			
		||||
		file?: MiDriveFile | null;
 | 
			
		||||
		uri?: string | null;
 | 
			
		||||
	}): Promise<Packed<'ChatMessageLite'>> {
 | 
			
		||||
		const memberships = await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id });
 | 
			
		||||
 | 
			
		||||
		if (toRoom.ownerId !== fromUser.id && !memberships.some(member => member.userId === fromUser.id)) {
 | 
			
		||||
			throw new Error('you are not a member of the room');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const message = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			fromUserId: fromUser.id,
 | 
			
		||||
			toRoomId: toRoom.id,
 | 
			
		||||
			text: params.text ? params.text.trim() : null,
 | 
			
		||||
			fileId: params.file ? params.file.id : null,
 | 
			
		||||
			reads: [],
 | 
			
		||||
			uri: params.uri ?? null,
 | 
			
		||||
		} satisfies Partial<MiChatMessage>;
 | 
			
		||||
 | 
			
		||||
		const inserted = await this.chatMessagesRepository.insertOne(message);
 | 
			
		||||
 | 
			
		||||
		const packedMessage = await this.chatEntityService.packMessageLiteForRoom(inserted);
 | 
			
		||||
 | 
			
		||||
		this.globalEventService.publishChatRoomStream(toRoom.id, 'message', packedMessage);
 | 
			
		||||
 | 
			
		||||
		const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
		for (const membership of memberships) {
 | 
			
		||||
			redisPipeline.set(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`, message.id);
 | 
			
		||||
			redisPipeline.sadd(`newChatMessagesExists:${membership.userId}`, `room:${toRoom.id}`);
 | 
			
		||||
		}
 | 
			
		||||
		redisPipeline.exec();
 | 
			
		||||
 | 
			
		||||
		// 3秒経っても既読にならなかったらイベント発行
 | 
			
		||||
		setTimeout(async () => {
 | 
			
		||||
			const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
			for (const membership of memberships) {
 | 
			
		||||
				redisPipeline.get(`newRoomChatMessageExists:${membership.userId}:${toRoom.id}`);
 | 
			
		||||
			}
 | 
			
		||||
			const markers = await redisPipeline.exec();
 | 
			
		||||
 | 
			
		||||
			if (markers.every(marker => marker[1] == null)) return;
 | 
			
		||||
 | 
			
		||||
			const packedMessageForTo = await this.chatEntityService.packMessageDetailed(inserted);
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < memberships.length; i++) {
 | 
			
		||||
				const marker = markers[i][1];
 | 
			
		||||
				if (marker == null) continue;
 | 
			
		||||
 | 
			
		||||
				this.globalEventService.publishMainStream(memberships[i].userId, 'newChatMessage', packedMessageForTo);
 | 
			
		||||
				this.pushNotificationService.pushNotification(memberships[i].userId, 'newChatMessage', packedMessageForTo);
 | 
			
		||||
			}
 | 
			
		||||
		}, 3000);
 | 
			
		||||
 | 
			
		||||
		return packedMessage;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async readUserChatMessage(
 | 
			
		||||
		readerId: MiUser['id'],
 | 
			
		||||
		senderId: MiUser['id'],
 | 
			
		||||
	): Promise<void> {
 | 
			
		||||
		const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
		redisPipeline.del(`newUserChatMessageExists:${readerId}:${senderId}`);
 | 
			
		||||
		redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`);
 | 
			
		||||
		await redisPipeline.exec();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async readRoomChatMessage(
 | 
			
		||||
		readerId: MiUser['id'],
 | 
			
		||||
		roomId: MiChatRoom['id'],
 | 
			
		||||
	): Promise<void> {
 | 
			
		||||
		const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
		redisPipeline.del(`newRoomChatMessageExists:${readerId}:${roomId}`);
 | 
			
		||||
		redisPipeline.srem(`newChatMessagesExists:${readerId}`, `room:${roomId}`);
 | 
			
		||||
		await redisPipeline.exec();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public findMessageById(messageId: MiChatMessage['id']) {
 | 
			
		||||
		return this.chatMessagesRepository.findOneBy({ id: messageId });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public findMyMessageById(userId: MiUser['id'], messageId: MiChatMessage['id']) {
 | 
			
		||||
		return this.chatMessagesRepository.findOneBy({ id: messageId, fromUserId: userId });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async deleteMessage(message: MiChatMessage) {
 | 
			
		||||
		await this.chatMessagesRepository.delete(message.id);
 | 
			
		||||
 | 
			
		||||
		if (message.toUserId) {
 | 
			
		||||
			const [fromUser, toUser] = await Promise.all([
 | 
			
		||||
				this.usersRepository.findOneByOrFail({ id: message.fromUserId }),
 | 
			
		||||
				this.usersRepository.findOneByOrFail({ id: message.toUserId }),
 | 
			
		||||
			]);
 | 
			
		||||
 | 
			
		||||
			if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatUserStream(message.fromUserId, message.toUserId, 'deleted', message.id);
 | 
			
		||||
			if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatUserStream(message.toUserId, message.fromUserId, 'deleted', message.id);
 | 
			
		||||
 | 
			
		||||
			if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
 | 
			
		||||
				//const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
 | 
			
		||||
				//this.queueService.deliver(fromUser, activity, toUser.inbox);
 | 
			
		||||
			}
 | 
			
		||||
		} else if (message.toRoomId) {
 | 
			
		||||
			this.globalEventService.publishChatRoomStream(message.toRoomId, 'deleted', message.id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async userTimeline(meId: MiUser['id'], otherId: MiUser['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
 | 
			
		||||
		const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
 | 
			
		||||
			.andWhere(new Brackets(qb => {
 | 
			
		||||
				qb
 | 
			
		||||
					.where(new Brackets(qb => {
 | 
			
		||||
						qb
 | 
			
		||||
							.where('message.fromUserId = :meId')
 | 
			
		||||
							.andWhere('message.toUserId = :otherId');
 | 
			
		||||
					}))
 | 
			
		||||
					.orWhere(new Brackets(qb => {
 | 
			
		||||
						qb
 | 
			
		||||
							.where('message.fromUserId = :otherId')
 | 
			
		||||
							.andWhere('message.toUserId = :meId');
 | 
			
		||||
					}));
 | 
			
		||||
			}))
 | 
			
		||||
			.setParameter('meId', meId)
 | 
			
		||||
			.setParameter('otherId', otherId);
 | 
			
		||||
 | 
			
		||||
		const messages = await query.take(limit).getMany();
 | 
			
		||||
 | 
			
		||||
		return messages;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async roomTimeline(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatMessage['id'] | null, untilId?: MiChatMessage['id'] | null) {
 | 
			
		||||
		const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), sinceId, untilId)
 | 
			
		||||
			.where('message.toRoomId = :roomId', { roomId });
 | 
			
		||||
 | 
			
		||||
		const messages = await query.take(limit).getMany();
 | 
			
		||||
 | 
			
		||||
		return messages;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async userHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
 | 
			
		||||
		const history: MiChatMessage[] = [];
 | 
			
		||||
 | 
			
		||||
		const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
 | 
			
		||||
			.select('muting.muteeId')
 | 
			
		||||
			.where('muting.muterId = :muterId', { muterId: meId });
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < limit; i++) {
 | 
			
		||||
			const found = history.map(m => (m.fromUserId === meId) ? m.toUserId! : m.fromUserId!);
 | 
			
		||||
 | 
			
		||||
			const query = this.chatMessagesRepository.createQueryBuilder('message')
 | 
			
		||||
				.orderBy('message.id', 'DESC')
 | 
			
		||||
				.where(new Brackets(qb => {
 | 
			
		||||
					qb
 | 
			
		||||
						.where('message.fromUserId = :meId', { meId: meId })
 | 
			
		||||
						.orWhere('message.toUserId = :meId', { meId: meId });
 | 
			
		||||
				}))
 | 
			
		||||
				.andWhere('message.toRoomId IS NULL')
 | 
			
		||||
				.andWhere(`message.fromUserId NOT IN (${ mutingQuery.getQuery() })`)
 | 
			
		||||
				.andWhere(`message.toUserId NOT IN (${ mutingQuery.getQuery() })`);
 | 
			
		||||
 | 
			
		||||
			if (found.length > 0) {
 | 
			
		||||
				query.andWhere('message.fromUserId NOT IN (:...found)', { found: found });
 | 
			
		||||
				query.andWhere('message.toUserId NOT IN (:...found)', { found: found });
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			query.setParameters(mutingQuery.getParameters());
 | 
			
		||||
 | 
			
		||||
			const message = await query.getOne();
 | 
			
		||||
 | 
			
		||||
			if (message) {
 | 
			
		||||
				history.push(message);
 | 
			
		||||
			} else {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return history;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async roomHistory(meId: MiUser['id'], limit: number): Promise<MiChatMessage[]> {
 | 
			
		||||
		// TODO: 一回のクエリにまとめられるかも
 | 
			
		||||
		const [memberRoomIds, ownedRoomIds] = await Promise.all([
 | 
			
		||||
			this.chatRoomMembershipsRepository.findBy({
 | 
			
		||||
				userId: meId,
 | 
			
		||||
			}).then(xs => xs.map(x => x.roomId)),
 | 
			
		||||
			this.chatRoomsRepository.findBy({
 | 
			
		||||
				ownerId: meId,
 | 
			
		||||
			}).then(xs => xs.map(x => x.id)),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		const roomIds = memberRoomIds.concat(ownedRoomIds);
 | 
			
		||||
 | 
			
		||||
		if (memberRoomIds.length === 0 && ownedRoomIds.length === 0) {
 | 
			
		||||
			return [];
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const history: MiChatMessage[] = [];
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < limit; i++) {
 | 
			
		||||
			const found = history.map(m => m.toRoomId!);
 | 
			
		||||
 | 
			
		||||
			const query = this.chatMessagesRepository.createQueryBuilder('message')
 | 
			
		||||
				.orderBy('message.id', 'DESC')
 | 
			
		||||
				.where('message.toRoomId IN (:...roomIds)', { roomIds });
 | 
			
		||||
 | 
			
		||||
			if (found.length > 0) {
 | 
			
		||||
				query.andWhere('message.toRoomId NOT IN (:...found)', { found: found });
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const message = await query.getOne();
 | 
			
		||||
 | 
			
		||||
			if (message) {
 | 
			
		||||
				history.push(message);
 | 
			
		||||
			} else {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return history;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getUserReadStateMap(userId: MiUser['id'], otherIds: MiUser['id'][]) {
 | 
			
		||||
		const readStateMap: Record<MiUser['id'], boolean> = {};
 | 
			
		||||
 | 
			
		||||
		const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
 | 
			
		||||
		for (const otherId of otherIds) {
 | 
			
		||||
			redisPipeline.get(`newUserChatMessageExists:${userId}:${otherId}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const markers = await redisPipeline.exec();
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < otherIds.length; i++) {
 | 
			
		||||
			const marker = markers[i][1];
 | 
			
		||||
			readStateMap[otherIds[i]] = marker == null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return readStateMap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getRoomReadStateMap(userId: MiUser['id'], roomIds: MiChatRoom['id'][]) {
 | 
			
		||||
		const readStateMap: Record<MiChatRoom['id'], boolean> = {};
 | 
			
		||||
 | 
			
		||||
		const redisPipeline = this.redisClient.pipeline();
 | 
			
		||||
 | 
			
		||||
		for (const roomId of roomIds) {
 | 
			
		||||
			redisPipeline.get(`newRoomChatMessageExists:${userId}:${roomId}`);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const markers = await redisPipeline.exec();
 | 
			
		||||
 | 
			
		||||
		for (let i = 0; i < roomIds.length; i++) {
 | 
			
		||||
			const marker = markers[i][1];
 | 
			
		||||
			readStateMap[roomIds[i]] = marker == null;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return readStateMap;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async hasUnreadMessages(userId: MiUser['id']) {
 | 
			
		||||
		const card = await this.redisClient.scard(`newChatMessagesExists:${userId}`);
 | 
			
		||||
		return card > 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createRoom(owner: MiUser, name: string) {
 | 
			
		||||
		const room = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			name: name,
 | 
			
		||||
			ownerId: owner.id,
 | 
			
		||||
		} satisfies Partial<MiChatRoom>;
 | 
			
		||||
 | 
			
		||||
		const created = await this.chatRoomsRepository.insertOne(room);
 | 
			
		||||
 | 
			
		||||
		return created;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async deleteRoom(room: MiChatRoom) {
 | 
			
		||||
		await this.chatRoomsRepository.delete(room.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async findMyRoomById(ownerId: MiUser['id'], roomId: MiChatRoom['id']) {
 | 
			
		||||
		return this.chatRoomsRepository.findOneBy({ id: roomId, ownerId: ownerId });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async findRoomById(roomId: MiChatRoom['id']) {
 | 
			
		||||
		return this.chatRoomsRepository.findOneBy({ id: roomId });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async isRoomMember(roomId: MiChatRoom['id'], userId: MiUser['id']) {
 | 
			
		||||
		const membership = await this.chatRoomMembershipsRepository.findOneBy({ roomId, userId });
 | 
			
		||||
		return membership != null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createRoomInvitation(inviterId: MiUser['id'], roomId: MiChatRoom['id'], inviteeId: MiUser['id']) {
 | 
			
		||||
		if (inviterId === inviteeId) {
 | 
			
		||||
			throw new Error('yourself');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const room = await this.chatRoomsRepository.findOneByOrFail({ id: roomId, ownerId: inviterId });
 | 
			
		||||
 | 
			
		||||
		const existingInvitation = await this.chatRoomInvitationsRepository.findOneBy({ roomId, userId: inviteeId });
 | 
			
		||||
		if (existingInvitation) {
 | 
			
		||||
			throw new Error('already invited');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
 | 
			
		||||
		if (membershipsCount >= MAX_ROOM_MEMBERS) {
 | 
			
		||||
			throw new Error('room is full');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: cehck block
 | 
			
		||||
 | 
			
		||||
		const invitation = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			roomId: room.id,
 | 
			
		||||
			userId: inviteeId,
 | 
			
		||||
		} satisfies Partial<MiChatRoomInvitation>;
 | 
			
		||||
 | 
			
		||||
		const created = await this.chatRoomInvitationsRepository.insertOne(invitation);
 | 
			
		||||
 | 
			
		||||
		return created;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getOwnedRoomsWithPagination(ownerId: MiUser['id'], limit: number, sinceId?: MiChatRoom['id'] | null, untilId?: MiChatRoom['id'] | null) {
 | 
			
		||||
		const query = this.queryService.makePaginationQuery(this.chatRoomsRepository.createQueryBuilder('room'), sinceId, untilId)
 | 
			
		||||
			.where('room.ownerId = :ownerId', { ownerId });
 | 
			
		||||
 | 
			
		||||
		const rooms = await query.take(limit).getMany();
 | 
			
		||||
 | 
			
		||||
		return rooms;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getReceivedRoomInvitationsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiChatRoomInvitation['id'] | null, untilId?: MiChatRoomInvitation['id'] | null) {
 | 
			
		||||
		const query = this.queryService.makePaginationQuery(this.chatRoomInvitationsRepository.createQueryBuilder('invitation'), sinceId, untilId)
 | 
			
		||||
			.where('invitation.userId = :userId', { userId })
 | 
			
		||||
			.andWhere('invitation.ignored = FALSE');
 | 
			
		||||
 | 
			
		||||
		const invitations = await query.take(limit).getMany();
 | 
			
		||||
 | 
			
		||||
		return invitations;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async joinToRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
 | 
			
		||||
		const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
 | 
			
		||||
 | 
			
		||||
		const membershipsCount = await this.chatRoomMembershipsRepository.countBy({ roomId });
 | 
			
		||||
		if (membershipsCount >= MAX_ROOM_MEMBERS) {
 | 
			
		||||
			throw new Error('room is full');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const membership = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			roomId: roomId,
 | 
			
		||||
			userId: userId,
 | 
			
		||||
		} satisfies Partial<MiChatRoomMembership>;
 | 
			
		||||
 | 
			
		||||
		// TODO: transaction
 | 
			
		||||
		await this.chatRoomMembershipsRepository.insertOne(membership);
 | 
			
		||||
		await this.chatRoomInvitationsRepository.delete(invitation.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async ignoreRoomInvitation(userId: MiUser['id'], roomId: MiChatRoom['id']) {
 | 
			
		||||
		const invitation = await this.chatRoomInvitationsRepository.findOneByOrFail({ roomId, userId });
 | 
			
		||||
		await this.chatRoomInvitationsRepository.update(invitation.id, { ignored: true });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async leaveRoom(userId: MiUser['id'], roomId: MiChatRoom['id']) {
 | 
			
		||||
		const membership = await this.chatRoomMembershipsRepository.findOneByOrFail({ roomId, userId });
 | 
			
		||||
		await this.chatRoomMembershipsRepository.delete(membership.id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updateRoom(room: MiChatRoom, params: {
 | 
			
		||||
		name?: string;
 | 
			
		||||
	}): Promise<MiChatRoom> {
 | 
			
		||||
		return this.chatRoomsRepository.createQueryBuilder().update()
 | 
			
		||||
			.set(params)
 | 
			
		||||
			.where('id = :id', { id: room.id })
 | 
			
		||||
			.returning('*')
 | 
			
		||||
			.execute()
 | 
			
		||||
			.then((response) => {
 | 
			
		||||
				return response.raw[0];
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async getRoomMembershipsWithPagination(roomId: MiChatRoom['id'], limit: number, sinceId?: MiChatRoomMembership['id'] | null, untilId?: MiChatRoomMembership['id'] | null) {
 | 
			
		||||
		const query = this.queryService.makePaginationQuery(this.chatRoomMembershipsRepository.createQueryBuilder('membership'), sinceId, untilId)
 | 
			
		||||
			.where('membership.roomId = :roomId', { roomId });
 | 
			
		||||
 | 
			
		||||
		const memberships = await query.take(limit).getMany();
 | 
			
		||||
 | 
			
		||||
		return memberships;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -44,6 +44,7 @@ import { ModerationLogService } from './ModerationLogService.js';
 | 
			
		||||
import { NoteCreateService } from './NoteCreateService.js';
 | 
			
		||||
import { NoteDeleteService } from './NoteDeleteService.js';
 | 
			
		||||
import { NotePiningService } from './NotePiningService.js';
 | 
			
		||||
import { NoteReadService } from './NoteReadService.js';
 | 
			
		||||
import { NotificationService } from './NotificationService.js';
 | 
			
		||||
import { PollService } from './PollService.js';
 | 
			
		||||
import { PushNotificationService } from './PushNotificationService.js';
 | 
			
		||||
@@ -74,7 +75,6 @@ import { ClipService } from './ClipService.js';
 | 
			
		||||
import { FeaturedService } from './FeaturedService.js';
 | 
			
		||||
import { FanoutTimelineService } from './FanoutTimelineService.js';
 | 
			
		||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
 | 
			
		||||
import { ChatService } from './ChatService.js';
 | 
			
		||||
import { RegistryApiService } from './RegistryApiService.js';
 | 
			
		||||
import { ReversiService } from './ReversiService.js';
 | 
			
		||||
 | 
			
		||||
@@ -100,7 +100,6 @@ import { AppEntityService } from './entities/AppEntityService.js';
 | 
			
		||||
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
 | 
			
		||||
import { BlockingEntityService } from './entities/BlockingEntityService.js';
 | 
			
		||||
import { ChannelEntityService } from './entities/ChannelEntityService.js';
 | 
			
		||||
import { ChatEntityService } from './entities/ChatEntityService.js';
 | 
			
		||||
import { ClipEntityService } from './entities/ClipEntityService.js';
 | 
			
		||||
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
 | 
			
		||||
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
 | 
			
		||||
@@ -185,6 +184,7 @@ const $ModerationLogService: Provider = { provide: 'ModerationLogService', useEx
 | 
			
		||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
 | 
			
		||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
 | 
			
		||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
 | 
			
		||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
 | 
			
		||||
const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService };
 | 
			
		||||
const $PollService: Provider = { provide: 'PollService', useExisting: PollService };
 | 
			
		||||
const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService };
 | 
			
		||||
@@ -221,7 +221,6 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
 | 
			
		||||
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
 | 
			
		||||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
 | 
			
		||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
 | 
			
		||||
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
 | 
			
		||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
 | 
			
		||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
 | 
			
		||||
 | 
			
		||||
@@ -248,7 +247,6 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting:
 | 
			
		||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
 | 
			
		||||
const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService };
 | 
			
		||||
const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService };
 | 
			
		||||
const $ChatEntityService: Provider = { provide: 'ChatEntityService', useExisting: ChatEntityService };
 | 
			
		||||
const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService };
 | 
			
		||||
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
 | 
			
		||||
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
 | 
			
		||||
@@ -335,6 +333,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		NoteCreateService,
 | 
			
		||||
		NoteDeleteService,
 | 
			
		||||
		NotePiningService,
 | 
			
		||||
		NoteReadService,
 | 
			
		||||
		NotificationService,
 | 
			
		||||
		PollService,
 | 
			
		||||
		SystemAccountService,
 | 
			
		||||
@@ -371,7 +370,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		FanoutTimelineService,
 | 
			
		||||
		FanoutTimelineEndpointService,
 | 
			
		||||
		ChannelFollowingService,
 | 
			
		||||
		ChatService,
 | 
			
		||||
		RegistryApiService,
 | 
			
		||||
		ReversiService,
 | 
			
		||||
 | 
			
		||||
@@ -398,7 +396,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		AuthSessionEntityService,
 | 
			
		||||
		BlockingEntityService,
 | 
			
		||||
		ChannelEntityService,
 | 
			
		||||
		ChatEntityService,
 | 
			
		||||
		ClipEntityService,
 | 
			
		||||
		DriveFileEntityService,
 | 
			
		||||
		DriveFolderEntityService,
 | 
			
		||||
@@ -481,6 +478,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$NoteCreateService,
 | 
			
		||||
		$NoteDeleteService,
 | 
			
		||||
		$NotePiningService,
 | 
			
		||||
		$NoteReadService,
 | 
			
		||||
		$NotificationService,
 | 
			
		||||
		$PollService,
 | 
			
		||||
		$SystemAccountService,
 | 
			
		||||
@@ -517,7 +515,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$FanoutTimelineService,
 | 
			
		||||
		$FanoutTimelineEndpointService,
 | 
			
		||||
		$ChannelFollowingService,
 | 
			
		||||
		$ChatService,
 | 
			
		||||
		$RegistryApiService,
 | 
			
		||||
		$ReversiService,
 | 
			
		||||
 | 
			
		||||
@@ -544,7 +541,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$AuthSessionEntityService,
 | 
			
		||||
		$BlockingEntityService,
 | 
			
		||||
		$ChannelEntityService,
 | 
			
		||||
		$ChatEntityService,
 | 
			
		||||
		$ClipEntityService,
 | 
			
		||||
		$DriveFileEntityService,
 | 
			
		||||
		$DriveFolderEntityService,
 | 
			
		||||
@@ -628,6 +624,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		NoteCreateService,
 | 
			
		||||
		NoteDeleteService,
 | 
			
		||||
		NotePiningService,
 | 
			
		||||
		NoteReadService,
 | 
			
		||||
		NotificationService,
 | 
			
		||||
		PollService,
 | 
			
		||||
		SystemAccountService,
 | 
			
		||||
@@ -664,7 +661,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		FanoutTimelineService,
 | 
			
		||||
		FanoutTimelineEndpointService,
 | 
			
		||||
		ChannelFollowingService,
 | 
			
		||||
		ChatService,
 | 
			
		||||
		RegistryApiService,
 | 
			
		||||
		ReversiService,
 | 
			
		||||
 | 
			
		||||
@@ -690,7 +686,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		AuthSessionEntityService,
 | 
			
		||||
		BlockingEntityService,
 | 
			
		||||
		ChannelEntityService,
 | 
			
		||||
		ChatEntityService,
 | 
			
		||||
		ClipEntityService,
 | 
			
		||||
		DriveFileEntityService,
 | 
			
		||||
		DriveFolderEntityService,
 | 
			
		||||
@@ -773,6 +768,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$NoteCreateService,
 | 
			
		||||
		$NoteDeleteService,
 | 
			
		||||
		$NotePiningService,
 | 
			
		||||
		$NoteReadService,
 | 
			
		||||
		$NotificationService,
 | 
			
		||||
		$PollService,
 | 
			
		||||
		$SystemAccountService,
 | 
			
		||||
@@ -808,7 +804,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$FanoutTimelineService,
 | 
			
		||||
		$FanoutTimelineEndpointService,
 | 
			
		||||
		$ChannelFollowingService,
 | 
			
		||||
		$ChatService,
 | 
			
		||||
		$RegistryApiService,
 | 
			
		||||
		$ReversiService,
 | 
			
		||||
 | 
			
		||||
@@ -834,7 +829,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$AuthSessionEntityService,
 | 
			
		||||
		$BlockingEntityService,
 | 
			
		||||
		$ChannelEntityService,
 | 
			
		||||
		$ChatEntityService,
 | 
			
		||||
		$ClipEntityService,
 | 
			
		||||
		$DriveFileEntityService,
 | 
			
		||||
		$DriveFolderEntityService,
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import type { MiPage } from '@/models/Page.js';
 | 
			
		||||
import type { MiWebhook } from '@/models/Webhook.js';
 | 
			
		||||
import type { MiSystemWebhook } from '@/models/SystemWebhook.js';
 | 
			
		||||
import type { MiMeta } from '@/models/Meta.js';
 | 
			
		||||
import { MiAvatarDecoration, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
 | 
			
		||||
import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
@@ -72,8 +72,12 @@ export interface MainEventTypes {
 | 
			
		||||
	readAllNotifications: undefined;
 | 
			
		||||
	notificationFlushed: undefined;
 | 
			
		||||
	unreadNotification: Packed<'Notification'>;
 | 
			
		||||
	unreadMention: MiNote['id'];
 | 
			
		||||
	readAllUnreadMentions: undefined;
 | 
			
		||||
	unreadSpecifiedNote: MiNote['id'];
 | 
			
		||||
	readAllUnreadSpecifiedNotes: undefined;
 | 
			
		||||
	readAllAntennas: undefined;
 | 
			
		||||
	unreadAntenna: MiAntenna;
 | 
			
		||||
	newChatMessage: Packed<'ChatMessage'>;
 | 
			
		||||
	readAllAnnouncements: undefined;
 | 
			
		||||
	myTokenRegenerated: undefined;
 | 
			
		||||
	signin: {
 | 
			
		||||
@@ -159,11 +163,6 @@ export interface AdminEventTypes {
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ChatEventTypes {
 | 
			
		||||
	message: Packed<'ChatMessage'>;
 | 
			
		||||
	deleted: Packed<'ChatMessage'>['id'];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ReversiEventTypes {
 | 
			
		||||
	matched: {
 | 
			
		||||
		game: Packed<'ReversiGameDetailed'>;
 | 
			
		||||
@@ -203,7 +202,7 @@ export interface ReversiGameEventTypes {
 | 
			
		||||
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
 | 
			
		||||
type EventUnionFromDictionary<
 | 
			
		||||
	T extends object,
 | 
			
		||||
	U = Events<T>,
 | 
			
		||||
	U = Events<T>
 | 
			
		||||
> = U[keyof U];
 | 
			
		||||
 | 
			
		||||
type SerializedAll<T> = {
 | 
			
		||||
@@ -296,14 +295,6 @@ export type GlobalEvents = {
 | 
			
		||||
		name: 'notesStream';
 | 
			
		||||
		payload: Serialized<Packed<'Note'>>;
 | 
			
		||||
	};
 | 
			
		||||
	chat: {
 | 
			
		||||
		name: `chatUserStream:${MiUser['id']}-${MiUser['id']}`;
 | 
			
		||||
		payload: EventTypesToEventPayload<ChatEventTypes>;
 | 
			
		||||
	};
 | 
			
		||||
	chatRoom: {
 | 
			
		||||
		name: `chatRoomStream:${MiChatRoom['id']}`;
 | 
			
		||||
		payload: EventTypesToEventPayload<ChatEventTypes>;
 | 
			
		||||
	};
 | 
			
		||||
	reversi: {
 | 
			
		||||
		name: `reversiStream:${MiUser['id']}`;
 | 
			
		||||
		payload: EventTypesToEventPayload<ReversiEventTypes>;
 | 
			
		||||
@@ -402,16 +393,6 @@ export class GlobalEventService {
 | 
			
		||||
		this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishChatUserStream<K extends keyof ChatEventTypes>(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void {
 | 
			
		||||
		this.publish(`chatUserStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishChatRoomStream<K extends keyof ChatEventTypes>(toRoomId: MiChatRoom['id'], type: K, value?: ChatEventTypes[K]): void {
 | 
			
		||||
		this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
 | 
			
		||||
		this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
			
		||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
 | 
			
		||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
@@ -198,6 +199,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
		private fanoutTimelineService: FanoutTimelineService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private relayService: RelayService,
 | 
			
		||||
		private federatedInstanceService: FederatedInstanceService,
 | 
			
		||||
@@ -580,6 +582,31 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
		if (!silent) {
 | 
			
		||||
			if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
 | 
			
		||||
 | 
			
		||||
			// 未読通知を作成
 | 
			
		||||
			if (data.visibility === 'specified') {
 | 
			
		||||
				if (data.visibleUsers == null) throw new Error('invalid param');
 | 
			
		||||
 | 
			
		||||
				for (const u of data.visibleUsers) {
 | 
			
		||||
					// ローカルユーザーのみ
 | 
			
		||||
					if (!this.userEntityService.isLocalUser(u)) continue;
 | 
			
		||||
 | 
			
		||||
					this.noteReadService.insertNoteUnread(u.id, note, {
 | 
			
		||||
						isSpecified: true,
 | 
			
		||||
						isMentioned: false,
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				for (const u of mentionedUsers) {
 | 
			
		||||
					// ローカルユーザーのみ
 | 
			
		||||
					if (!this.userEntityService.isLocalUser(u)) continue;
 | 
			
		||||
 | 
			
		||||
					this.noteReadService.insertNoteUnread(u.id, note, {
 | 
			
		||||
						isSpecified: false,
 | 
			
		||||
						isMentioned: true,
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Pack the note
 | 
			
		||||
			const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								packages/backend/src/core/NoteReadService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								packages/backend/src/core/NoteReadService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setTimeout } from 'node:timers/promises';
 | 
			
		||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class NoteReadService implements OnApplicationShutdown {
 | 
			
		||||
	#shutdownController = new AbortController();
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.noteUnreadsRepository)
 | 
			
		||||
		private noteUnreadsRepository: NoteUnreadsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.mutingsRepository)
 | 
			
		||||
		private mutingsRepository: MutingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.noteThreadMutingsRepository)
 | 
			
		||||
		private noteThreadMutingsRepository: NoteThreadMutingsRepository,
 | 
			
		||||
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async insertNoteUnread(userId: MiUser['id'], note: MiNote, params: {
 | 
			
		||||
		// NOTE: isSpecifiedがtrueならisMentionedは必ずfalse
 | 
			
		||||
		isSpecified: boolean;
 | 
			
		||||
		isMentioned: boolean;
 | 
			
		||||
	}): Promise<void> {
 | 
			
		||||
		//#region ミュートしているなら無視
 | 
			
		||||
		const mute = await this.mutingsRepository.findBy({
 | 
			
		||||
			muterId: userId,
 | 
			
		||||
		});
 | 
			
		||||
		if (mute.map(m => m.muteeId).includes(note.userId)) return;
 | 
			
		||||
		//#endregion
 | 
			
		||||
 | 
			
		||||
		// スレッドミュート
 | 
			
		||||
		const isThreadMuted = await this.noteThreadMutingsRepository.exists({
 | 
			
		||||
			where: {
 | 
			
		||||
				userId: userId,
 | 
			
		||||
				threadId: note.threadId ?? note.id,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		if (isThreadMuted) return;
 | 
			
		||||
 | 
			
		||||
		const unread = {
 | 
			
		||||
			id: this.idService.gen(),
 | 
			
		||||
			noteId: note.id,
 | 
			
		||||
			userId: userId,
 | 
			
		||||
			isSpecified: params.isSpecified,
 | 
			
		||||
			isMentioned: params.isMentioned,
 | 
			
		||||
			noteUserId: note.userId,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		await this.noteUnreadsRepository.insert(unread);
 | 
			
		||||
 | 
			
		||||
		// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
 | 
			
		||||
		setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
 | 
			
		||||
			const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
 | 
			
		||||
 | 
			
		||||
			if (!exist) return;
 | 
			
		||||
 | 
			
		||||
			if (params.isMentioned) {
 | 
			
		||||
				this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
 | 
			
		||||
			}
 | 
			
		||||
			if (params.isSpecified) {
 | 
			
		||||
				this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
 | 
			
		||||
			}
 | 
			
		||||
		}, () => { /* aborted, ignore it */ });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async read(
 | 
			
		||||
		userId: MiUser['id'],
 | 
			
		||||
		notes: (MiNote | Packed<'Note'>)[],
 | 
			
		||||
	): Promise<void> {
 | 
			
		||||
		if (notes.length === 0) return;
 | 
			
		||||
 | 
			
		||||
		const noteIds = new Set<MiNote['id']>();
 | 
			
		||||
 | 
			
		||||
		for (const note of notes) {
 | 
			
		||||
			if (note.mentions && note.mentions.includes(userId)) {
 | 
			
		||||
				noteIds.add(note.id);
 | 
			
		||||
			} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
 | 
			
		||||
				noteIds.add(note.id);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (noteIds.size === 0) return;
 | 
			
		||||
 | 
			
		||||
		// Remove the record
 | 
			
		||||
		await this.noteUnreadsRepository.delete({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
			noteId: In(Array.from(noteIds)),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// TODO: ↓まとめてクエリしたい
 | 
			
		||||
 | 
			
		||||
		trackPromise(this.noteUnreadsRepository.countBy({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
			isMentioned: true,
 | 
			
		||||
		}).then(mentionsCount => {
 | 
			
		||||
			if (mentionsCount === 0) {
 | 
			
		||||
				// 全て既読になったイベントを発行
 | 
			
		||||
				this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
 | 
			
		||||
			}
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		trackPromise(this.noteUnreadsRepository.countBy({
 | 
			
		||||
			userId: userId,
 | 
			
		||||
			isSpecified: true,
 | 
			
		||||
		}).then(specifiedCount => {
 | 
			
		||||
			if (specifiedCount === 0) {
 | 
			
		||||
				// 全て既読になったイベントを発行
 | 
			
		||||
				this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
 | 
			
		||||
			}
 | 
			
		||||
		}));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose(): void {
 | 
			
		||||
		this.#shutdownController.abort();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public onApplicationShutdown(signal?: string | undefined): void {
 | 
			
		||||
		this.dispose();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -63,7 +63,6 @@ export type RolePolicies = {
 | 
			
		||||
	canImportFollowing: boolean;
 | 
			
		||||
	canImportMuting: boolean;
 | 
			
		||||
	canImportUserLists: boolean;
 | 
			
		||||
	canChat: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
@@ -98,7 +97,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
 | 
			
		||||
	canImportFollowing: true,
 | 
			
		||||
	canImportMuting: true,
 | 
			
		||||
	canImportUserLists: true,
 | 
			
		||||
	canChat: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
@@ -402,7 +400,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
 | 
			
		||||
			canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
 | 
			
		||||
			canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
 | 
			
		||||
			canChat: calc('canChat', vs => vs.some(v => v === true)),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
 | 
			
		||||
import { ModuleRef } from '@nestjs/core';
 | 
			
		||||
import { Brackets, IsNull } from 'typeorm';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
 | 
			
		||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
@@ -736,30 +736,4 @@ export class UserFollowingService implements OnModuleInit {
 | 
			
		||||
			.where('following.followerId = :followerId', { followerId: userId })
 | 
			
		||||
			.getMany();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public isFollowing(followerId: MiUser['id'], followeeId: MiUser['id']) {
 | 
			
		||||
		return this.followingsRepository.exists({
 | 
			
		||||
			where: {
 | 
			
		||||
				followerId,
 | 
			
		||||
				followeeId,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
 | 
			
		||||
		const count = await this.followingsRepository.createQueryBuilder('following')
 | 
			
		||||
			.where(new Brackets(qb => {
 | 
			
		||||
				qb.where('following.followerId = :aUserId', { aUserId })
 | 
			
		||||
					.andWhere('following.followeeId = :bUserId', { bUserId });
 | 
			
		||||
			}))
 | 
			
		||||
			.orWhere(new Brackets(qb => {
 | 
			
		||||
				qb.where('following.followerId = :bUserId', { bUserId })
 | 
			
		||||
					.andWhere('following.followeeId = :aUserId', { aUserId });
 | 
			
		||||
			}))
 | 
			
		||||
			.getCount();
 | 
			
		||||
 | 
			
		||||
		return count === 2;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,282 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { MiUser, ChatMessagesRepository, MiChatMessage, ChatRoomsRepository, MiChatRoom, MiChatRoomInvitation, ChatRoomInvitationsRepository, MiChatRoomMembership, ChatRoomMembershipsRepository } from '@/models/_.js';
 | 
			
		||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import type { } from '@/models/Blocking.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { UserEntityService } from './UserEntityService.js';
 | 
			
		||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ChatEntityService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.chatMessagesRepository)
 | 
			
		||||
		private chatMessagesRepository: ChatMessagesRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomsRepository)
 | 
			
		||||
		private chatRoomsRepository: ChatRoomsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomInvitationsRepository)
 | 
			
		||||
		private chatRoomInvitationsRepository: ChatRoomInvitationsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.chatRoomMembershipsRepository)
 | 
			
		||||
		private chatRoomMembershipsRepository: ChatRoomMembershipsRepository,
 | 
			
		||||
 | 
			
		||||
		private userEntityService: UserEntityService,
 | 
			
		||||
		private driveFileEntityService: DriveFileEntityService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessageDetailed(
 | 
			
		||||
		src: MiChatMessage['id'] | MiChatMessage,
 | 
			
		||||
		me?: { id: MiUser['id'] },
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
 | 
			
		||||
				packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
 | 
			
		||||
				packedRooms: Map<MiChatMessage['toRoomId'], Packed<'ChatRoom'> | null>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatMessage'>> {
 | 
			
		||||
		const packedUsers = options?._hint_?.packedUsers;
 | 
			
		||||
		const packedFiles = options?._hint_?.packedFiles;
 | 
			
		||||
		const packedRooms = options?._hint_?.packedRooms;
 | 
			
		||||
 | 
			
		||||
		const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: message.id,
 | 
			
		||||
			createdAt: this.idService.parse(message.id).date.toISOString(),
 | 
			
		||||
			text: message.text,
 | 
			
		||||
			fromUserId: message.fromUserId,
 | 
			
		||||
			fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me),
 | 
			
		||||
			toUserId: message.toUserId,
 | 
			
		||||
			toUser: message.toUserId ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined,
 | 
			
		||||
			toRoomId: message.toRoomId,
 | 
			
		||||
			toRoom: message.toRoomId ? (packedRooms?.get(message.toRoomId) ?? await this.packRoom(message.toRoom ?? message.toRoomId, me)) : undefined,
 | 
			
		||||
			fileId: message.fileId,
 | 
			
		||||
			file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessagesDetailed(
 | 
			
		||||
		messages: MiChatMessage[],
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
	) {
 | 
			
		||||
		if (messages.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const excludeMe = (x: MiUser | string) => {
 | 
			
		||||
			if (typeof x === 'string') {
 | 
			
		||||
				return x !== me.id;
 | 
			
		||||
			} else {
 | 
			
		||||
				return x.id !== me.id;
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const users = [
 | 
			
		||||
			...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe),
 | 
			
		||||
			...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe),
 | 
			
		||||
		];
 | 
			
		||||
 | 
			
		||||
		const [packedUsers, packedFiles] = await Promise.all([
 | 
			
		||||
			this.userEntityService.packMany(users, me)
 | 
			
		||||
				.then(users => new Map(users.map(u => [u.id, u]))),
 | 
			
		||||
			this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		return Promise.all(messages.map(message => this.packMessageDetailed(message, me, { _hint_: { packedUsers, packedFiles } })));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessageLite(
 | 
			
		||||
		src: MiChatMessage['id'] | MiChatMessage,
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatMessageLite'>> {
 | 
			
		||||
		const packedFiles = options?._hint_?.packedFiles;
 | 
			
		||||
 | 
			
		||||
		const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: message.id,
 | 
			
		||||
			createdAt: this.idService.parse(message.id).date.toISOString(),
 | 
			
		||||
			text: message.text,
 | 
			
		||||
			fromUserId: message.fromUserId,
 | 
			
		||||
			toUserId: message.toUserId,
 | 
			
		||||
			fileId: message.fileId,
 | 
			
		||||
			file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessagesLite(
 | 
			
		||||
		messages: MiChatMessage[],
 | 
			
		||||
	) {
 | 
			
		||||
		if (messages.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const [packedFiles] = await Promise.all([
 | 
			
		||||
			this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		return Promise.all(messages.map(message => this.packMessageLite(message, { _hint_: { packedFiles } })));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessageLiteForRoom(
 | 
			
		||||
		src: MiChatMessage['id'] | MiChatMessage,
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
 | 
			
		||||
				packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatMessageLite'>> {
 | 
			
		||||
		const packedFiles = options?._hint_?.packedFiles;
 | 
			
		||||
		const packedUsers = options?._hint_?.packedUsers;
 | 
			
		||||
 | 
			
		||||
		const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: message.id,
 | 
			
		||||
			createdAt: this.idService.parse(message.id).date.toISOString(),
 | 
			
		||||
			text: message.text,
 | 
			
		||||
			fromUserId: message.fromUserId,
 | 
			
		||||
			fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
 | 
			
		||||
			toRoomId: message.toRoomId,
 | 
			
		||||
			fileId: message.fileId,
 | 
			
		||||
			file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packMessagesLiteForRoom(
 | 
			
		||||
		messages: MiChatMessage[],
 | 
			
		||||
	) {
 | 
			
		||||
		if (messages.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const [packedUsers, packedFiles] = await Promise.all([
 | 
			
		||||
			this.userEntityService.packMany(messages.map(x => x.fromUser ?? x.fromUserId))
 | 
			
		||||
				.then(users => new Map(users.map(u => [u.id, u]))),
 | 
			
		||||
			this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		return Promise.all(messages.map(message => this.packMessageLiteForRoom(message, { _hint_: { packedFiles, packedUsers } })));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRoom(
 | 
			
		||||
		src: MiChatRoom['id'] | MiChatRoom,
 | 
			
		||||
		me?: { id: MiUser['id'] },
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedOwners: Map<MiChatRoom['id'], Packed<'UserLite'>>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatRoom'>> {
 | 
			
		||||
		const room = typeof src === 'object' ? src : await this.chatRoomsRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: room.id,
 | 
			
		||||
			createdAt: this.idService.parse(room.id).date.toISOString(),
 | 
			
		||||
			name: room.name,
 | 
			
		||||
			ownerId: room.ownerId,
 | 
			
		||||
			owner: options?._hint_?.packedOwners.get(room.ownerId) ?? await this.userEntityService.pack(room.owner ?? room.ownerId, me),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRooms(
 | 
			
		||||
		rooms: MiChatRoom[],
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
	) {
 | 
			
		||||
		if (rooms.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const owners = rooms.map(x => x.owner ?? x.ownerId);
 | 
			
		||||
 | 
			
		||||
		const packedOwners = await this.userEntityService.packMany(owners, me)
 | 
			
		||||
			.then(users => new Map(users.map(u => [u.id, u])));
 | 
			
		||||
 | 
			
		||||
		return Promise.all(rooms.map(room => this.packRoom(room, me, { _hint_: { packedOwners } })));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRoomInvitation(
 | 
			
		||||
		src: MiChatRoomInvitation['id'] | MiChatRoomInvitation,
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedRooms: Map<MiChatRoomInvitation['roomId'], Packed<'ChatRoom'>>;
 | 
			
		||||
				packedUsers: Map<MiChatRoomInvitation['id'], Packed<'UserLite'>>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatRoomInvitation'>> {
 | 
			
		||||
		const invitation = typeof src === 'object' ? src : await this.chatRoomInvitationsRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: invitation.id,
 | 
			
		||||
			createdAt: this.idService.parse(invitation.id).date.toISOString(),
 | 
			
		||||
			roomId: invitation.roomId,
 | 
			
		||||
			room: options?._hint_?.packedRooms.get(invitation.roomId) ?? await this.packRoom(invitation.room ?? invitation.roomId, me),
 | 
			
		||||
			userId: invitation.userId,
 | 
			
		||||
			user: options?._hint_?.packedUsers.get(invitation.userId) ?? await this.userEntityService.pack(invitation.user ?? invitation.userId, me),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRoomInvitations(
 | 
			
		||||
		invitations: MiChatRoomInvitation[],
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
	) {
 | 
			
		||||
		if (invitations.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		return Promise.all(invitations.map(invitation => this.packRoomInvitation(invitation, me)));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRoomMembership(
 | 
			
		||||
		src: MiChatRoomMembership['id'] | MiChatRoomMembership,
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
		options?: {
 | 
			
		||||
			_hint_?: {
 | 
			
		||||
				packedUsers: Map<MiChatRoomMembership['id'], Packed<'UserLite'>>;
 | 
			
		||||
			};
 | 
			
		||||
		},
 | 
			
		||||
	): Promise<Packed<'ChatRoomMembership'>> {
 | 
			
		||||
		const membership = typeof src === 'object' ? src : await this.chatRoomMembershipsRepository.findOneByOrFail({ id: src });
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			id: membership.id,
 | 
			
		||||
			createdAt: this.idService.parse(membership.id).date.toISOString(),
 | 
			
		||||
			userId: membership.userId,
 | 
			
		||||
			user: options?._hint_?.packedUsers.get(membership.userId) ?? await this.userEntityService.pack(membership.user ?? membership.userId, me),
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async packRoomMemberships(
 | 
			
		||||
		memberships: MiChatRoomMembership[],
 | 
			
		||||
		me: { id: MiUser['id'] },
 | 
			
		||||
	) {
 | 
			
		||||
		if (memberships.length === 0) return [];
 | 
			
		||||
 | 
			
		||||
		const users = memberships.map(x => x.user ?? x.userId);
 | 
			
		||||
 | 
			
		||||
		const packedUsers = await this.userEntityService.packMany(users, me)
 | 
			
		||||
			.then(users => new Map(users.map(u => [u.id, u])));
 | 
			
		||||
 | 
			
		||||
		return Promise.all(memberships.map(membership => this.packRoomMembership(membership, me, { _hint_: { packedUsers } })));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -32,6 +32,7 @@ import type {
 | 
			
		||||
	MiUserNotePining,
 | 
			
		||||
	MiUserProfile,
 | 
			
		||||
	MutingsRepository,
 | 
			
		||||
	NoteUnreadsRepository,
 | 
			
		||||
	RenoteMutingsRepository,
 | 
			
		||||
	UserMemoRepository,
 | 
			
		||||
	UserNotePiningsRepository,
 | 
			
		||||
@@ -47,9 +48,9 @@ import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
 | 
			
		||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
 | 
			
		||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import type { OnModuleInit } from '@nestjs/common';
 | 
			
		||||
import type { NoteEntityService } from './NoteEntityService.js';
 | 
			
		||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
 | 
			
		||||
import type { PageEntityService } from './PageEntityService.js';
 | 
			
		||||
 | 
			
		||||
const Ajv = _Ajv.default;
 | 
			
		||||
@@ -93,7 +94,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		||||
	private federatedInstanceService: FederatedInstanceService;
 | 
			
		||||
	private idService: IdService;
 | 
			
		||||
	private avatarDecorationService: AvatarDecorationService;
 | 
			
		||||
	private chatService: ChatService;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private moduleRef: ModuleRef,
 | 
			
		||||
@@ -128,6 +128,9 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		||||
		@Inject(DI.renoteMutingsRepository)
 | 
			
		||||
		private renoteMutingsRepository: RenoteMutingsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.noteUnreadsRepository)
 | 
			
		||||
		private noteUnreadsRepository: NoteUnreadsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.userNotePiningsRepository)
 | 
			
		||||
		private userNotePiningsRepository: UserNotePiningsRepository,
 | 
			
		||||
 | 
			
		||||
@@ -149,7 +152,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		||||
		this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
 | 
			
		||||
		this.idService = this.moduleRef.get('IdService');
 | 
			
		||||
		this.avatarDecorationService = this.moduleRef.get('AvatarDecorationService');
 | 
			
		||||
		this.chatService = this.moduleRef.get('ChatService');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Validators
 | 
			
		||||
@@ -556,7 +558,6 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		||||
				publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
 | 
			
		||||
				followersVisibility: profile!.followersVisibility,
 | 
			
		||||
				followingVisibility: profile!.followingVisibility,
 | 
			
		||||
				chatScope: user.chatScope,
 | 
			
		||||
				roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
 | 
			
		||||
					id: role.id,
 | 
			
		||||
					name: role.name,
 | 
			
		||||
@@ -597,9 +598,14 @@ export class UserEntityService implements OnModuleInit {
 | 
			
		||||
				isDeleted: user.isDeleted,
 | 
			
		||||
				twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
 | 
			
		||||
				hideOnlineStatus: user.hideOnlineStatus,
 | 
			
		||||
				hasUnreadSpecifiedNotes: false, // 後方互換性のため
 | 
			
		||||
				hasUnreadMentions: false, // 後方互換性のため
 | 
			
		||||
				hasUnreadChatMessages: this.chatService.hasUnreadMessages(user.id),
 | 
			
		||||
				hasUnreadSpecifiedNotes: this.noteUnreadsRepository.count({
 | 
			
		||||
					where: { userId: user.id, isSpecified: true },
 | 
			
		||||
					take: 1,
 | 
			
		||||
				}).then(count => count > 0),
 | 
			
		||||
				hasUnreadMentions: this.noteUnreadsRepository.count({
 | 
			
		||||
					where: { userId: user.id, isMentioned: true },
 | 
			
		||||
					take: 1,
 | 
			
		||||
				}).then(count => count > 0),
 | 
			
		||||
				hasUnreadAnnouncement: unreadAnnouncements!.length > 0,
 | 
			
		||||
				unreadAnnouncements,
 | 
			
		||||
				hasUnreadAntenna: this.getHasUnreadAntenna(user.id),
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ export const DI = {
 | 
			
		||||
	noteFavoritesRepository: Symbol('noteFavoritesRepository'),
 | 
			
		||||
	noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
 | 
			
		||||
	noteReactionsRepository: Symbol('noteReactionsRepository'),
 | 
			
		||||
	noteUnreadsRepository: Symbol('noteUnreadsRepository'),
 | 
			
		||||
	pollsRepository: Symbol('pollsRepository'),
 | 
			
		||||
	pollVotesRepository: Symbol('pollVotesRepository'),
 | 
			
		||||
	userProfilesRepository: Symbol('userProfilesRepository'),
 | 
			
		||||
@@ -82,11 +83,6 @@ export const DI = {
 | 
			
		||||
	flashsRepository: Symbol('flashsRepository'),
 | 
			
		||||
	flashLikesRepository: Symbol('flashLikesRepository'),
 | 
			
		||||
	userMemosRepository: Symbol('userMemosRepository'),
 | 
			
		||||
	chatMessagesRepository: Symbol('chatMessagesRepository'),
 | 
			
		||||
	chatApprovalsRepository: Symbol('chatApprovalsRepository'),
 | 
			
		||||
	chatRoomsRepository: Symbol('chatRoomsRepository'),
 | 
			
		||||
	chatRoomMembershipsRepository: Symbol('chatRoomMembershipsRepository'),
 | 
			
		||||
	chatRoomInvitationsRepository: Symbol('chatRoomInvitationsRepository'),
 | 
			
		||||
	bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
 | 
			
		||||
	reversiGamesRepository: Symbol('reversiGamesRepository'),
 | 
			
		||||
	//#endregion
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import * as fs from 'node:fs/promises';
 | 
			
		||||
import { WritableStream } from 'node:stream/web';
 | 
			
		||||
import type { PathLike } from 'node:fs';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -63,10 +63,6 @@ import {
 | 
			
		||||
} from '@/models/json-schema/meta.js';
 | 
			
		||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
 | 
			
		||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
 | 
			
		||||
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
 | 
			
		||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
 | 
			
		||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
 | 
			
		||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
 | 
			
		||||
 | 
			
		||||
export const refs = {
 | 
			
		||||
	UserLite: packedUserLiteSchema,
 | 
			
		||||
@@ -124,11 +120,6 @@ export const refs = {
 | 
			
		||||
	MetaDetailed: packedMetaDetailedSchema,
 | 
			
		||||
	SystemWebhook: packedSystemWebhookSchema,
 | 
			
		||||
	AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
 | 
			
		||||
	ChatMessage: packedChatMessageSchema,
 | 
			
		||||
	ChatMessageLite: packedChatMessageLiteSchema,
 | 
			
		||||
	ChatRoom: packedChatRoomSchema,
 | 
			
		||||
	ChatRoomInvitation: packedChatRoomInvitationSchema,
 | 
			
		||||
	ChatRoomMembership: packedChatRoomMembershipSchema,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
 | 
			
		||||
@@ -180,11 +171,11 @@ export interface Schema extends OfSchema {
 | 
			
		||||
 | 
			
		||||
type RequiredPropertyNames<s extends Obj> = {
 | 
			
		||||
	[K in keyof s]:
 | 
			
		||||
	// K is not optional
 | 
			
		||||
	s[K]['optional'] extends false ? K :
 | 
			
		||||
	// K has default value
 | 
			
		||||
	s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
 | 
			
		||||
	never
 | 
			
		||||
		// K is not optional
 | 
			
		||||
		s[K]['optional'] extends false ? K :
 | 
			
		||||
		// K has default value
 | 
			
		||||
		s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
 | 
			
		||||
		never
 | 
			
		||||
}[keyof s];
 | 
			
		||||
 | 
			
		||||
export type Obj = Record<string, Schema>;
 | 
			
		||||
@@ -223,18 +214,18 @@ type ObjectSchemaTypeDef<p extends Schema> =
 | 
			
		||||
		p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
 | 
			
		||||
			UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
 | 
			
		||||
			: never
 | 
			
		||||
		: ObjType<p['properties'], NonNullable<p['required']>>
 | 
			
		||||
		:
 | 
			
		||||
		p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
 | 
			
		||||
		p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
 | 
			
		||||
		p['additionalProperties'] extends true ? Record<string, any> :
 | 
			
		||||
		p['additionalProperties'] extends Schema ?
 | 
			
		||||
			p['additionalProperties'] extends infer AdditionalProperties ?
 | 
			
		||||
				AdditionalProperties extends Schema ?
 | 
			
		||||
					Record<string, SchemaType<AdditionalProperties>> :
 | 
			
		||||
					never :
 | 
			
		||||
			: ObjType<p['properties'], NonNullable<p['required']>>
 | 
			
		||||
	:
 | 
			
		||||
	p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
 | 
			
		||||
	p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
 | 
			
		||||
	p['additionalProperties'] extends true ? Record<string, any> :
 | 
			
		||||
	p['additionalProperties'] extends Schema ?
 | 
			
		||||
		p['additionalProperties'] extends infer AdditionalProperties ?
 | 
			
		||||
			AdditionalProperties extends Schema ?
 | 
			
		||||
				Record<string, SchemaType<AdditionalProperties>> :
 | 
			
		||||
				never :
 | 
			
		||||
			any;
 | 
			
		||||
			never :
 | 
			
		||||
	any;
 | 
			
		||||
 | 
			
		||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
 | 
			
		||||
 | 
			
		||||
@@ -244,30 +235,30 @@ export type SchemaTypeDef<p extends Schema> =
 | 
			
		||||
	p['type'] extends 'number' ? number :
 | 
			
		||||
	p['type'] extends 'string' ? (
 | 
			
		||||
		p['enum'] extends readonly (string | null)[] ?
 | 
			
		||||
			p['enum'][number] :
 | 
			
		||||
			p['format'] extends 'date-time' ? string : // Dateにする??
 | 
			
		||||
			string
 | 
			
		||||
		p['enum'][number] :
 | 
			
		||||
		p['format'] extends 'date-time' ? string : // Dateにする??
 | 
			
		||||
		string
 | 
			
		||||
	) :
 | 
			
		||||
		p['type'] extends 'boolean' ? boolean :
 | 
			
		||||
		p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
 | 
			
		||||
		p['type'] extends 'array' ? (
 | 
			
		||||
			p['items'] extends OfSchema ? (
 | 
			
		||||
				p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
 | 
			
		||||
				p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
 | 
			
		||||
				p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
 | 
			
		||||
				never
 | 
			
		||||
			) :
 | 
			
		||||
				p['prefixItems'] extends ReadonlyArray<Schema> ? (
 | 
			
		||||
					p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
 | 
			
		||||
					p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
					p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
					[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
 | 
			
		||||
				) :
 | 
			
		||||
					p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
 | 
			
		||||
					any[]
 | 
			
		||||
	p['type'] extends 'boolean' ? boolean :
 | 
			
		||||
	p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
 | 
			
		||||
	p['type'] extends 'array' ? (
 | 
			
		||||
		p['items'] extends OfSchema ? (
 | 
			
		||||
			p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
 | 
			
		||||
			p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
 | 
			
		||||
			p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
 | 
			
		||||
			never
 | 
			
		||||
		) :
 | 
			
		||||
			p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
 | 
			
		||||
			p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
 | 
			
		||||
			any;
 | 
			
		||||
		p['prefixItems'] extends ReadonlyArray<Schema> ? (
 | 
			
		||||
			p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
 | 
			
		||||
			p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
			p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
 | 
			
		||||
			[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
 | 
			
		||||
		) :
 | 
			
		||||
		p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
 | 
			
		||||
		any[]
 | 
			
		||||
	) :
 | 
			
		||||
	p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
 | 
			
		||||
	p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
 | 
			
		||||
	any;
 | 
			
		||||
 | 
			
		||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
 | 
			
		||||
@Entity('chat_approval')
 | 
			
		||||
@Index(['userId', 'otherId'], { unique: true })
 | 
			
		||||
export class MiChatApproval {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public userId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public otherId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public other: MiUser | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,85 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiDriveFile } from './DriveFile.js';
 | 
			
		||||
import { MiChatRoom } from './ChatRoom.js';
 | 
			
		||||
 | 
			
		||||
@Entity('chat_message')
 | 
			
		||||
export class MiChatMessage {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public fromUserId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public fromUser: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(), nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public toUserId: MiUser['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public toUser: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(), nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public toRoomId: MiChatRoom['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiChatRoom, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public toRoom: MiChatRoom | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 4096, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public text: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public uri: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		array: true, default: '{}',
 | 
			
		||||
	})
 | 
			
		||||
	public reads: MiUser['id'][];
 | 
			
		||||
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public fileId: MiDriveFile['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiDriveFile, {
 | 
			
		||||
		onDelete: 'SET NULL',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public file: MiDriveFile | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024, array: true, default: '{}',
 | 
			
		||||
	})
 | 
			
		||||
	public reactions: string[];
 | 
			
		||||
}
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
 | 
			
		||||
@Entity('chat_room')
 | 
			
		||||
export class MiChatRoom {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256,
 | 
			
		||||
	})
 | 
			
		||||
	public name: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public ownerId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public owner: MiUser | null;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiChatRoom } from './ChatRoom.js';
 | 
			
		||||
 | 
			
		||||
@Entity('chat_room_invitation')
 | 
			
		||||
@Index(['userId', 'roomId'], { unique: true })
 | 
			
		||||
export class MiChatRoomInvitation {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public userId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public roomId: MiChatRoom['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiChatRoom, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public room: MiChatRoom | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public ignored: boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiChatRoom } from './ChatRoom.js';
 | 
			
		||||
 | 
			
		||||
@Entity('chat_room_membership')
 | 
			
		||||
@Index(['userId', 'roomId'], { unique: true })
 | 
			
		||||
export class MiChatRoomMembership {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public userId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
	})
 | 
			
		||||
	public roomId: MiChatRoom['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiChatRoom, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public room: MiChatRoom | null;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								packages/backend/src/models/NoteUnread.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								packages/backend/src/models/NoteUnread.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
 | 
			
		||||
import { id } from './util/id.js';
 | 
			
		||||
import { MiUser } from './User.js';
 | 
			
		||||
import { MiNote } from './Note.js';
 | 
			
		||||
import type { MiChannel } from './Channel.js';
 | 
			
		||||
 | 
			
		||||
@Entity('note_unread')
 | 
			
		||||
@Index(['userId', 'noteId'], { unique: true })
 | 
			
		||||
export class MiNoteUnread {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public userId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiUser, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public user: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column(id())
 | 
			
		||||
	public noteId: MiNote['id'];
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => MiNote, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
	})
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public note: MiNote | null;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * メンションか否か
 | 
			
		||||
	 */
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('boolean')
 | 
			
		||||
	public isMentioned: boolean;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * ダイレクト投稿か否か
 | 
			
		||||
	 */
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('boolean')
 | 
			
		||||
	public isSpecified: boolean;
 | 
			
		||||
 | 
			
		||||
	//#region Denormalized fields
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		comment: '[Denormalized]',
 | 
			
		||||
	})
 | 
			
		||||
	public noteUserId: MiUser['id'];
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		comment: '[Denormalized]',
 | 
			
		||||
	})
 | 
			
		||||
	public noteChannelId: MiChannel['id'] | null;
 | 
			
		||||
	//#endregion
 | 
			
		||||
}
 | 
			
		||||
@@ -42,6 +42,7 @@ import {
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
	MiPageLike,
 | 
			
		||||
	MiPasswordResetRequest,
 | 
			
		||||
@@ -77,11 +78,6 @@ import {
 | 
			
		||||
	MiUserPublickey,
 | 
			
		||||
	MiUserSecurityKey,
 | 
			
		||||
	MiWebhook,
 | 
			
		||||
	MiChatMessage,
 | 
			
		||||
	MiChatRoom,
 | 
			
		||||
	MiChatRoomMembership,
 | 
			
		||||
	MiChatRoomInvitation,
 | 
			
		||||
	MiChatApproval,
 | 
			
		||||
} from './_.js';
 | 
			
		||||
import type { Provider } from '@nestjs/common';
 | 
			
		||||
import type { DataSource } from 'typeorm';
 | 
			
		||||
@@ -140,6 +136,12 @@ const $noteReactionsRepository: Provider = {
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $noteUnreadsRepository: Provider = {
 | 
			
		||||
	provide: DI.noteUnreadsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $pollsRepository: Provider = {
 | 
			
		||||
	provide: DI.pollsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
 | 
			
		||||
@@ -286,7 +288,7 @@ const $swSubscriptionsRepository: Provider = {
 | 
			
		||||
 | 
			
		||||
const $systemAccountsRepository: Provider = {
 | 
			
		||||
	provide: DI.systemAccountsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiSystemAccount).extend(miRepository as MiRepository<MiSystemAccount>),
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiSystemAccount),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +306,7 @@ const $abuseUserReportsRepository: Provider = {
 | 
			
		||||
 | 
			
		||||
const $abuseReportNotificationRecipientRepository: Provider = {
 | 
			
		||||
	provide: DI.abuseReportNotificationRecipientRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient).extend(miRepository as MiRepository<MiAbuseReportNotificationRecipient>),
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -436,7 +438,7 @@ const $webhooksRepository: Provider = {
 | 
			
		||||
 | 
			
		||||
const $systemWebhooksRepository: Provider = {
 | 
			
		||||
	provide: DI.systemWebhooksRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook).extend(miRepository as MiRepository<MiSystemWebhook>),
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -488,36 +490,6 @@ const $userMemosRepository: Provider = {
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $chatMessagesRepository: Provider = {
 | 
			
		||||
	provide: DI.chatMessagesRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiChatMessage).extend(miRepository as MiRepository<MiChatMessage>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $chatRoomsRepository: Provider = {
 | 
			
		||||
	provide: DI.chatRoomsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiChatRoom).extend(miRepository as MiRepository<MiChatRoom>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $chatRoomMembershipsRepository: Provider = {
 | 
			
		||||
	provide: DI.chatRoomMembershipsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiChatRoomMembership).extend(miRepository as MiRepository<MiChatRoomMembership>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $chatRoomInvitationsRepository: Provider = {
 | 
			
		||||
	provide: DI.chatRoomInvitationsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiChatRoomInvitation).extend(miRepository as MiRepository<MiChatRoomInvitation>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $chatApprovalsRepository: Provider = {
 | 
			
		||||
	provide: DI.chatApprovalsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiChatApproval).extend(miRepository as MiRepository<MiChatApproval>),
 | 
			
		||||
	inject: [DI.db],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const $bubbleGameRecordsRepository: Provider = {
 | 
			
		||||
	provide: DI.bubbleGameRecordsRepository,
 | 
			
		||||
	useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
 | 
			
		||||
@@ -542,6 +514,7 @@ const $reversiGamesRepository: Provider = {
 | 
			
		||||
		$noteFavoritesRepository,
 | 
			
		||||
		$noteThreadMutingsRepository,
 | 
			
		||||
		$noteReactionsRepository,
 | 
			
		||||
		$noteUnreadsRepository,
 | 
			
		||||
		$pollsRepository,
 | 
			
		||||
		$pollVotesRepository,
 | 
			
		||||
		$userProfilesRepository,
 | 
			
		||||
@@ -600,11 +573,6 @@ const $reversiGamesRepository: Provider = {
 | 
			
		||||
		$flashsRepository,
 | 
			
		||||
		$flashLikesRepository,
 | 
			
		||||
		$userMemosRepository,
 | 
			
		||||
		$chatMessagesRepository,
 | 
			
		||||
		$chatRoomsRepository,
 | 
			
		||||
		$chatRoomMembershipsRepository,
 | 
			
		||||
		$chatRoomInvitationsRepository,
 | 
			
		||||
		$chatApprovalsRepository,
 | 
			
		||||
		$bubbleGameRecordsRepository,
 | 
			
		||||
		$reversiGamesRepository,
 | 
			
		||||
	],
 | 
			
		||||
@@ -618,6 +586,7 @@ const $reversiGamesRepository: Provider = {
 | 
			
		||||
		$noteFavoritesRepository,
 | 
			
		||||
		$noteThreadMutingsRepository,
 | 
			
		||||
		$noteReactionsRepository,
 | 
			
		||||
		$noteUnreadsRepository,
 | 
			
		||||
		$pollsRepository,
 | 
			
		||||
		$pollVotesRepository,
 | 
			
		||||
		$userProfilesRepository,
 | 
			
		||||
@@ -676,11 +645,6 @@ const $reversiGamesRepository: Provider = {
 | 
			
		||||
		$flashsRepository,
 | 
			
		||||
		$flashLikesRepository,
 | 
			
		||||
		$userMemosRepository,
 | 
			
		||||
		$chatMessagesRepository,
 | 
			
		||||
		$chatRoomsRepository,
 | 
			
		||||
		$chatRoomMembershipsRepository,
 | 
			
		||||
		$chatRoomInvitationsRepository,
 | 
			
		||||
		$chatApprovalsRepository,
 | 
			
		||||
		$bubbleGameRecordsRepository,
 | 
			
		||||
		$reversiGamesRepository,
 | 
			
		||||
	],
 | 
			
		||||
 
 | 
			
		||||
@@ -225,17 +225,6 @@ export class MiUser {
 | 
			
		||||
	})
 | 
			
		||||
	public emojis: string[];
 | 
			
		||||
 | 
			
		||||
	// チャットを許可する相手
 | 
			
		||||
	// everyone: 誰からでも
 | 
			
		||||
	// followers: フォロワーのみ
 | 
			
		||||
	// following: フォローしているユーザーのみ
 | 
			
		||||
	// mutual: 相互フォローのみ
 | 
			
		||||
	// none: 誰からも受け付けない
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, default: 'mutual',
 | 
			
		||||
	})
 | 
			
		||||
	public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, nullable: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,13 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
 | 
			
		||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
 | 
			
		||||
import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
 | 
			
		||||
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
 | 
			
		||||
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
 | 
			
		||||
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
 | 
			
		||||
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
 | 
			
		||||
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
 | 
			
		||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
 | 
			
		||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
 | 
			
		||||
import { MiAccessToken } from '@/models/AccessToken.js';
 | 
			
		||||
@@ -40,6 +43,7 @@ import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
 | 
			
		||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
 | 
			
		||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
 | 
			
		||||
import { MiNoteUnread } from '@/models/NoteUnread.js';
 | 
			
		||||
import { MiPage } from '@/models/Page.js';
 | 
			
		||||
import { MiPageLike } from '@/models/PageLike.js';
 | 
			
		||||
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
 | 
			
		||||
@@ -74,11 +78,6 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
 | 
			
		||||
import { MiFlash } from '@/models/Flash.js';
 | 
			
		||||
import { MiFlashLike } from '@/models/FlashLike.js';
 | 
			
		||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
 | 
			
		||||
import { MiChatMessage } from '@/models/ChatMessage.js';
 | 
			
		||||
import { MiChatRoom } from '@/models/ChatRoom.js';
 | 
			
		||||
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
 | 
			
		||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
 | 
			
		||||
import { MiChatApproval } from '@/models/ChatApproval.js';
 | 
			
		||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
 | 
			
		||||
import { MiReversiGame } from '@/models/ReversiGame.js';
 | 
			
		||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
 | 
			
		||||
@@ -160,6 +159,7 @@ export {
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
	MiPageLike,
 | 
			
		||||
	MiPasswordResetRequest,
 | 
			
		||||
@@ -194,11 +194,6 @@ export {
 | 
			
		||||
	MiFlash,
 | 
			
		||||
	MiFlashLike,
 | 
			
		||||
	MiUserMemo,
 | 
			
		||||
	MiChatMessage,
 | 
			
		||||
	MiChatRoom,
 | 
			
		||||
	MiChatRoomMembership,
 | 
			
		||||
	MiChatRoomInvitation,
 | 
			
		||||
	MiChatApproval,
 | 
			
		||||
	MiBubbleGameRecord,
 | 
			
		||||
	MiReversiGame,
 | 
			
		||||
};
 | 
			
		||||
@@ -236,6 +231,7 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
 | 
			
		||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
 | 
			
		||||
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
 | 
			
		||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
 | 
			
		||||
export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
 | 
			
		||||
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
 | 
			
		||||
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
 | 
			
		||||
export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
 | 
			
		||||
@@ -270,10 +266,5 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiReposit
 | 
			
		||||
export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
 | 
			
		||||
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
 | 
			
		||||
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
 | 
			
		||||
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
 | 
			
		||||
export type ChatRoomsRepository = Repository<MiChatRoom> & MiRepository<MiChatRoom>;
 | 
			
		||||
export type ChatRoomMembershipsRepository = Repository<MiChatRoomMembership> & MiRepository<MiChatRoomMembership>;
 | 
			
		||||
export type ChatRoomInvitationsRepository = Repository<MiChatRoomInvitation> & MiRepository<MiChatRoomInvitation>;
 | 
			
		||||
export type ChatApprovalsRepository = Repository<MiChatApproval> & MiRepository<MiChatApproval>;
 | 
			
		||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
 | 
			
		||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const packedChatMessageSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		fromUserId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		fromUser: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
		toUserId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		toUser: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
		toRoomId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		toRoom: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'ChatRoom',
 | 
			
		||||
		},
 | 
			
		||||
		text: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		fileId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		file: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'DriveFile',
 | 
			
		||||
		},
 | 
			
		||||
		isRead: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: true, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const packedChatMessageLiteSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		fromUserId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		toUserId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		toRoomId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		text: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		fileId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
		},
 | 
			
		||||
		file: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: true, nullable: true,
 | 
			
		||||
			ref: 'DriveFile',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const packedChatRoomInvitationSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		userId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		user: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
		roomId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		room: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatRoom',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const packedChatRoomMembershipSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		userId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		user: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const packedChatRoomSchema = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		id: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		createdAt: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			format: 'date-time',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		ownerId: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		owner: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'UserLite',
 | 
			
		||||
		},
 | 
			
		||||
		name: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
@@ -292,10 +292,6 @@ export const packedRolePoliciesSchema = {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
		canChat: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -358,11 +358,6 @@ export const packedUserDetailedNotMeOnlySchema = {
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			enum: ['public', 'followers', 'private'],
 | 
			
		||||
		},
 | 
			
		||||
		chatScope: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
			enum: ['everyone', 'following', 'followers', 'mutual', 'none'],
 | 
			
		||||
		},
 | 
			
		||||
		roles: {
 | 
			
		||||
			type: 'array',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
@@ -545,10 +540,6 @@ export const packedMeDetailedOnlySchema = {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		hasUnreadChatMessages: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
		},
 | 
			
		||||
		hasUnreadNotification: {
 | 
			
		||||
			type: 'boolean',
 | 
			
		||||
			nullable: false, optional: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,6 @@ import pg from 'pg';
 | 
			
		||||
import { DataSource, Logger } from 'typeorm';
 | 
			
		||||
import * as highlight from 'cli-highlight';
 | 
			
		||||
import { entities as charts } from '@/core/chart/entities.js';
 | 
			
		||||
import { Config } from '@/config.js';
 | 
			
		||||
import MisskeyLogger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
 | 
			
		||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
 | 
			
		||||
@@ -45,6 +42,7 @@ import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
 | 
			
		||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
 | 
			
		||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
 | 
			
		||||
import { MiNoteUnread } from '@/models/NoteUnread.js';
 | 
			
		||||
import { MiPage } from '@/models/Page.js';
 | 
			
		||||
import { MiPageLike } from '@/models/PageLike.js';
 | 
			
		||||
import { MiPasswordResetRequest } from '@/models/PasswordResetRequest.js';
 | 
			
		||||
@@ -78,14 +76,13 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
 | 
			
		||||
import { MiFlash } from '@/models/Flash.js';
 | 
			
		||||
import { MiFlashLike } from '@/models/FlashLike.js';
 | 
			
		||||
import { MiUserMemo } from '@/models/UserMemo.js';
 | 
			
		||||
import { MiChatMessage } from '@/models/ChatMessage.js';
 | 
			
		||||
import { MiChatRoom } from '@/models/ChatRoom.js';
 | 
			
		||||
import { MiChatRoomMembership } from '@/models/ChatRoomMembership.js';
 | 
			
		||||
import { MiChatRoomInvitation } from '@/models/ChatRoomInvitation.js';
 | 
			
		||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
 | 
			
		||||
import { MiReversiGame } from '@/models/ReversiGame.js';
 | 
			
		||||
import { MiChatApproval } from '@/models/ChatApproval.js';
 | 
			
		||||
import { MiSystemAccount } from '@/models/SystemAccount.js';
 | 
			
		||||
 | 
			
		||||
import { Config } from '@/config.js';
 | 
			
		||||
import MisskeyLogger from '@/logger.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MiSystemAccount } from './models/SystemAccount.js';
 | 
			
		||||
 | 
			
		||||
pg.types.setTypeParser(20, Number);
 | 
			
		||||
 | 
			
		||||
@@ -198,6 +195,7 @@ export const entities = [
 | 
			
		||||
	MiNoteFavorite,
 | 
			
		||||
	MiNoteReaction,
 | 
			
		||||
	MiNoteThreadMuting,
 | 
			
		||||
	MiNoteUnread,
 | 
			
		||||
	MiPage,
 | 
			
		||||
	MiPageLike,
 | 
			
		||||
	MiGalleryPost,
 | 
			
		||||
@@ -238,11 +236,6 @@ export const entities = [
 | 
			
		||||
	MiFlash,
 | 
			
		||||
	MiFlashLike,
 | 
			
		||||
	MiUserMemo,
 | 
			
		||||
	MiChatMessage,
 | 
			
		||||
	MiChatRoom,
 | 
			
		||||
	MiChatRoomMembership,
 | 
			
		||||
	MiChatRoomInvitation,
 | 
			
		||||
	MiChatApproval,
 | 
			
		||||
	MiBubbleGameRecord,
 | 
			
		||||
	MiReversiGame,
 | 
			
		||||
	...charts,
 | 
			
		||||
 
 | 
			
		||||
@@ -44,8 +44,6 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
 | 
			
		||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
 | 
			
		||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
 | 
			
		||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
 | 
			
		||||
import { ChatUserChannelService } from './api/stream/channels/chat-user.js';
 | 
			
		||||
import { ChatRoomChannelService } from './api/stream/channels/chat-room.js';
 | 
			
		||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
 | 
			
		||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
 | 
			
		||||
import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
 | 
			
		||||
@@ -86,8 +84,6 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
 | 
			
		||||
		GlobalTimelineChannelService,
 | 
			
		||||
		HashtagChannelService,
 | 
			
		||||
		RoleTimelineChannelService,
 | 
			
		||||
		ChatUserChannelService,
 | 
			
		||||
		ChatRoomChannelService,
 | 
			
		||||
		ReversiChannelService,
 | 
			
		||||
		ReversiGameChannelService,
 | 
			
		||||
		HomeTimelineChannelService,
 | 
			
		||||
 
 | 
			
		||||
@@ -391,10 +391,10 @@ export class ApiCallService implements OnApplicationShutdown {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
 | 
			
		||||
		if (ep.meta.requireRolePolicy != null && (this.meta.rootUserId !== user!.id)) {
 | 
			
		||||
			const myRoles = await this.roleService.getUserRoles(user!.id);
 | 
			
		||||
			const policies = await this.roleService.getUserPolicies(user!.id);
 | 
			
		||||
			if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
 | 
			
		||||
			if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
 | 
			
		||||
				throw new ApiError({
 | 
			
		||||
					message: 'You are not assigned to a required role.',
 | 
			
		||||
					code: 'ROLE_PERMISSION_DENIED',
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import * as Redis from 'ioredis';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
@@ -34,6 +35,7 @@ export class StreamingApiServerService {
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		private cacheService: CacheService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
		private authenticateService: AuthenticateService,
 | 
			
		||||
		private channelsService: ChannelsService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
@@ -94,6 +96,7 @@ export class StreamingApiServerService {
 | 
			
		||||
 | 
			
		||||
			const stream = new MainStreamConnection(
 | 
			
		||||
				this.channelsService,
 | 
			
		||||
				this.noteReadService,
 | 
			
		||||
				this.notificationService,
 | 
			
		||||
				this.cacheService,
 | 
			
		||||
				this.channelFollowingService,
 | 
			
		||||
 
 | 
			
		||||
@@ -263,6 +263,7 @@ export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.
 | 
			
		||||
export * as 'i/page-likes' from './endpoints/i/page-likes.js';
 | 
			
		||||
export * as 'i/pages' from './endpoints/i/pages.js';
 | 
			
		||||
export * as 'i/pin' from './endpoints/i/pin.js';
 | 
			
		||||
export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
 | 
			
		||||
export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
 | 
			
		||||
export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
 | 
			
		||||
export * as 'i/registry/get' from './endpoints/i/registry/get.js';
 | 
			
		||||
@@ -396,22 +397,4 @@ export * as 'users/search' from './endpoints/users/search.js';
 | 
			
		||||
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
 | 
			
		||||
export * as 'users/show' from './endpoints/users/show.js';
 | 
			
		||||
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
 | 
			
		||||
export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
 | 
			
		||||
export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
 | 
			
		||||
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
 | 
			
		||||
export * as 'chat/messages/show' from './endpoints/chat/messages/show.js';
 | 
			
		||||
export * as 'chat/messages/user-timeline' from './endpoints/chat/messages/user-timeline.js';
 | 
			
		||||
export * as 'chat/messages/room-timeline' from './endpoints/chat/messages/room-timeline.js';
 | 
			
		||||
export * as 'chat/rooms/create' from './endpoints/chat/rooms/create.js';
 | 
			
		||||
export * as 'chat/rooms/delete' from './endpoints/chat/rooms/delete.js';
 | 
			
		||||
export * as 'chat/rooms/join' from './endpoints/chat/rooms/join.js';
 | 
			
		||||
export * as 'chat/rooms/leave' from './endpoints/chat/rooms/leave.js';
 | 
			
		||||
export * as 'chat/rooms/show' from './endpoints/chat/rooms/show.js';
 | 
			
		||||
export * as 'chat/rooms/owned' from './endpoints/chat/rooms/owned.js';
 | 
			
		||||
export * as 'chat/rooms/update' from './endpoints/chat/rooms/update.js';
 | 
			
		||||
export * as 'chat/rooms/members' from './endpoints/chat/rooms/members.js';
 | 
			
		||||
export * as 'chat/rooms/invitations/create' from './endpoints/chat/rooms/invitations/create.js';
 | 
			
		||||
export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitations/ignore.js';
 | 
			
		||||
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
 | 
			
		||||
export * as 'chat/history' from './endpoints/chat/history.js';
 | 
			
		||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ interface IEndpointMetaBase {
 | 
			
		||||
	 */
 | 
			
		||||
	readonly requireAdmin?: boolean;
 | 
			
		||||
 | 
			
		||||
	readonly requiredRolePolicy?: KeyOf<'RolePolicies'>;
 | 
			
		||||
	readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * 引っ越し済みのユーザーによるリクエストを禁止するか
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	requireRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	kind: 'write:admin:avatar-decorations',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	requireRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	kind: 'write:admin:avatar-decorations',
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	requireRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	kind: 'read:admin:avatar-decorations',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	requireRolePolicy: 'canManageAvatarDecorations',
 | 
			
		||||
	kind: 'write:admin:avatar-decorations',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'read:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'read:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'write:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { NotesRepository, AntennasRepository } from '@/models/_.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
@@ -58,6 +59,9 @@ export const paramDef = {
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.redisForTimelines)
 | 
			
		||||
		private redisForTimelines: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
 | 
			
		||||
@@ -67,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
		private fanoutTimelineService: FanoutTimelineService,
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -119,6 +124,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
				notes.sort((a, b) => a.id > b.id ? -1 : 1);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			this.noteReadService.read(me.id, notes);
 | 
			
		||||
 | 
			
		||||
			return await this.noteEntityService.packMany(notes, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatMessage',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		room: { type: 'boolean', default: false },
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
 | 
			
		||||
 | 
			
		||||
			const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);
 | 
			
		||||
 | 
			
		||||
			if (ps.room) {
 | 
			
		||||
				const roomIds = history.map(m => m.toRoomId!);
 | 
			
		||||
				const readStateMap = await this.chatService.getRoomReadStateMap(me.id, roomIds);
 | 
			
		||||
 | 
			
		||||
				for (const message of packedMessages) {
 | 
			
		||||
					message.isRead = readStateMap[message.toRoomId!] ?? false;
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				const otherIds = history.map(m => m.fromUserId === me.id ? m.toUserId! : m.fromUserId!);
 | 
			
		||||
				const readStateMap = await this.chatService.getUserReadStateMap(me.id, otherIds);
 | 
			
		||||
 | 
			
		||||
				for (const message of packedMessages) {
 | 
			
		||||
					const otherId = message.fromUserId === me.id ? message.toUserId! : message.fromUserId!;
 | 
			
		||||
					message.isRead = readStateMap[otherId] ?? false;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return packedMessages;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,105 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import type { DriveFilesRepository, MiUser } from '@/models/_.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canChat',
 | 
			
		||||
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 500,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatMessageLite',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '8098520d-2da5-4e8f-8ee1-df78b55a4ec6',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchFile: {
 | 
			
		||||
			message: 'No such file.',
 | 
			
		||||
			code: 'NO_SUCH_FILE',
 | 
			
		||||
			id: 'b6accbd3-1d7b-4d9f-bdb7-eb185bac06db',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		contentRequired: {
 | 
			
		||||
			message: 'Content required. You need to set text or fileId.',
 | 
			
		||||
			code: 'CONTENT_REQUIRED',
 | 
			
		||||
			id: '340517b7-6d04-42c0-bac1-37ee804e3594',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		text: { type: 'string', nullable: true, maxLength: 2000 },
 | 
			
		||||
		fileId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		toRoomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['toRoomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findRoomById(ps.toRoomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			let file = null;
 | 
			
		||||
			if (ps.fileId != null) {
 | 
			
		||||
				file = await this.driveFilesRepository.findOneBy({
 | 
			
		||||
					id: ps.fileId,
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (file == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchFile);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// テキストが無いかつ添付ファイルも無かったらエラー
 | 
			
		||||
			if (ps.text == null && file == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.contentRequired);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await this.chatService.createMessageToRoom(me, room, {
 | 
			
		||||
				text: ps.text,
 | 
			
		||||
				file: file,
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,122 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import type { DriveFilesRepository, MiUser } from '@/models/_.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canChat',
 | 
			
		||||
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
		max: 500,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatMessageLite',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		recipientIsYourself: {
 | 
			
		||||
			message: 'You can not send a message to yourself.',
 | 
			
		||||
			code: 'RECIPIENT_IS_YOURSELF',
 | 
			
		||||
			id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchUser: {
 | 
			
		||||
			message: 'No such user.',
 | 
			
		||||
			code: 'NO_SUCH_USER',
 | 
			
		||||
			id: '11795c64-40ea-4198-b06e-3c873ed9039d',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noSuchFile: {
 | 
			
		||||
			message: 'No such file.',
 | 
			
		||||
			code: 'NO_SUCH_FILE',
 | 
			
		||||
			id: '4372b8e2-185d-4146-8749-2f68864a3e5f',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		contentRequired: {
 | 
			
		||||
			message: 'Content required. You need to set text or fileId.',
 | 
			
		||||
			code: 'CONTENT_REQUIRED',
 | 
			
		||||
			id: '25587321-b0e6-449c-9239-f8925092942c',
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		youHaveBeenBlocked: {
 | 
			
		||||
			message: 'You cannot send a message because you have been blocked by this user.',
 | 
			
		||||
			code: 'YOU_HAVE_BEEN_BLOCKED',
 | 
			
		||||
			id: 'c15a5199-7422-4968-941a-2a462c478f7d',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		text: { type: 'string', nullable: true, maxLength: 2000 },
 | 
			
		||||
		fileId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		toUserId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['toUserId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.driveFilesRepository)
 | 
			
		||||
		private driveFilesRepository: DriveFilesRepository,
 | 
			
		||||
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			let file = null;
 | 
			
		||||
			if (ps.fileId != null) {
 | 
			
		||||
				file = await this.driveFilesRepository.findOneBy({
 | 
			
		||||
					id: ps.fileId,
 | 
			
		||||
					userId: me.id,
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				if (file == null) {
 | 
			
		||||
					throw new ApiError(meta.errors.noSuchFile);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// テキストが無いかつ添付ファイルも無かったらエラー
 | 
			
		||||
			if (ps.text == null && file == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.contentRequired);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Myself
 | 
			
		||||
			if (ps.toUserId === me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.recipientIsYourself);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const toUser = await this.getterService.getUser(ps.toUserId).catch(err => {
 | 
			
		||||
				if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 | 
			
		||||
				throw err;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return await this.chatService.createMessageToUser(me, toUser, {
 | 
			
		||||
				text: ps.text,
 | 
			
		||||
				file: file,
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchMessage: {
 | 
			
		||||
			message: 'No such message.',
 | 
			
		||||
			code: 'NO_SUCH_MESSAGE',
 | 
			
		||||
			id: '36b67f0e-66a6-414b-83df-992a55294f17',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		messageId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['messageId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
 | 
			
		||||
			if (message == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
			}
 | 
			
		||||
			await this.chatService.deleteMessage(message);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatMessageLite',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: 'c4d9f88c-9270-4632-b032-6ed8cee36f7f',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findRoomById(ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (!(await this.chatService.isRoomMember(room.id, me.id)) && room.ownerId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId);
 | 
			
		||||
 | 
			
		||||
			this.chatService.readRoomChatMessage(me.id, room.id);
 | 
			
		||||
 | 
			
		||||
			return await this.chatEntityService.packMessagesLiteForRoom(messages);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatMessage',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchMessage: {
 | 
			
		||||
			message: 'No such message.',
 | 
			
		||||
			code: 'NO_SUCH_MESSAGE',
 | 
			
		||||
			id: '3710865b-1848-4da9-8d61-cfed15510b93',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		messageId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['messageId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private roleService: RoleService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const message = await this.chatService.findMessageById(ps.messageId);
 | 
			
		||||
			if (message == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
			}
 | 
			
		||||
			if (message.fromUserId !== me.id && message.toUserId !== me.id && !(await this.roleService.isModerator(me))) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchMessage);
 | 
			
		||||
			}
 | 
			
		||||
			return this.chatEntityService.packMessageDetailed(message, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatMessageLite',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchUser: {
 | 
			
		||||
			message: 'No such user.',
 | 
			
		||||
			code: 'NO_SUCH_USER',
 | 
			
		||||
			id: '11795c64-40ea-4198-b06e-3c873ed9039d',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		userId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['userId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const other = await this.getterService.getUser(ps.userId).catch(err => {
 | 
			
		||||
				if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
 | 
			
		||||
				throw err;
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId);
 | 
			
		||||
 | 
			
		||||
			this.chatService.readUserChatMessage(me.id, other.id);
 | 
			
		||||
 | 
			
		||||
			return await this.chatEntityService.packMessagesLite(messages);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canChat',
 | 
			
		||||
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1day'),
 | 
			
		||||
		max: 10,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatRoom',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		name: { type: 'string', maxLength: 256 },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['name'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.createRoom(me, ps.name);
 | 
			
		||||
			return await this.chatEntityService.packRoom(room);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4b',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
			await this.chatService.deleteRoom(room);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,68 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canChat',
 | 
			
		||||
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1day'),
 | 
			
		||||
		max: 50,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatRoomInvitation',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '916f9507-49ba-4e90-b57f-1fd4deaa47a5',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		userId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId', 'userId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
			const invitation = await this.chatService.createRoomInvitation(me.id, room.id, ps.userId);
 | 
			
		||||
			return await this.chatEntityService.packRoomInvitation(invitation, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '5130557e-5a11-4cfb-9cc5-fe60cda5de0d',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			await this.chatService.ignoreRoomInvitation(me.id, ps.roomId);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatRoomInvitation',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
 | 
			
		||||
			return this.chatEntityService.packRoomInvitations(invitations, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '84416476-5ce8-4a2c-b568-9569f1b10733',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			await this.chatService.joinToRoom(me.id, ps.roomId);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: 'cb7f3179-50e8-4389-8c30-dbe2650a67c9',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			await this.chatService.leaveRoom(me.id, ps.roomId);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatRoomMembership',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '7b9fe84c-eafc-4d21-bf89-485458ed2c18',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findRoomById(ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (!(await this.chatService.isRoomMember(room.id, me.id)) && room.ownerId !== me.id) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId);
 | 
			
		||||
 | 
			
		||||
			return this.chatEntityService.packRoomMemberships(memberships, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'array',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		items: {
 | 
			
		||||
			type: 'object',
 | 
			
		||||
			optional: false, nullable: false,
 | 
			
		||||
			ref: 'ChatRoom',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
 | 
			
		||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
 | 
			
		||||
			return this.chatEntityService.packRooms(rooms, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'read:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatRoom',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: '857ae02f-8759-4d20-9adb-6e95fffe4fd7',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findRoomById(ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return this.chatEntityService.packRoom(room, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import { ApiError } from '@/server/api/error.js';
 | 
			
		||||
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['chat'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:chat',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
		type: 'object',
 | 
			
		||||
		optional: false, nullable: false,
 | 
			
		||||
		ref: 'ChatRoom',
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
		noSuchRoom: {
 | 
			
		||||
			message: 'No such room.',
 | 
			
		||||
			code: 'NO_SUCH_ROOM',
 | 
			
		||||
			id: 'fcdb0f92-bda6-47f9-bd05-343e0e020932',
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {
 | 
			
		||||
		roomId: { type: 'string', format: 'misskey:id' },
 | 
			
		||||
		name: { type: 'string', maxLength: 256 },
 | 
			
		||||
	},
 | 
			
		||||
	required: ['roomId'],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
		private chatEntityService: ChatEntityService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
 | 
			
		||||
			if (room == null) {
 | 
			
		||||
				throw new ApiError(meta.errors.noSuchRoom);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const updated = await this.chatService.updateRoom(room, {
 | 
			
		||||
				name: ps.name,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			return this.chatEntityService.packRoom(updated, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ import { ApiError } from '../../error.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canImportAntennas',
 | 
			
		||||
	requireRolePolicy: 'canImportAntennas',
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canImportBlocking',
 | 
			
		||||
	requireRolePolicy: 'canImportBlocking',
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canImportFollowing',
 | 
			
		||||
	requireRolePolicy: 'canImportFollowing',
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canImportMuting',
 | 
			
		||||
	requireRolePolicy: 'canImportMuting',
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
 | 
			
		||||
	limit: {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ import { ApiError } from '../../error.js';
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canImportUserLists',
 | 
			
		||||
	requireRolePolicy: 'canImportUserLists',
 | 
			
		||||
	prohibitMoved: true,
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1hour'),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { NotesRepository } from '@/models/_.js';
 | 
			
		||||
import { obsoleteNotificationTypes, groupedNotificationTypes, FilterUnionByProperty } from '@/types.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
 | 
			
		||||
import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
@@ -62,9 +63,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
		@Inject(DI.redis)
 | 
			
		||||
		private redisClient: Redis.Redis,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.notesRepository)
 | 
			
		||||
		private notesRepository: NotesRepository,
 | 
			
		||||
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private notificationEntityService: NotificationEntityService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const EXTRA_LIMIT = 100;
 | 
			
		||||
@@ -157,6 +162,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			groupedNotifications = groupedNotifications.slice(0, ps.limit);
 | 
			
		||||
			const noteIds = groupedNotifications
 | 
			
		||||
				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
 | 
			
		||||
				.map(notification => notification.noteId!);
 | 
			
		||||
 | 
			
		||||
			if (noteIds.length > 0) {
 | 
			
		||||
				const notes = await this.notesRepository.findBy({ id: In(noteIds) });
 | 
			
		||||
				this.noteReadService.read(me.id, notes);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await this.notificationEntityService.packGroupedMany(groupedNotifications, me.id);
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { NotesRepository } from '@/models/_.js';
 | 
			
		||||
import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
 | 
			
		||||
import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
@@ -68,6 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
		private notificationEntityService: NotificationEntityService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			// includeTypes が空の場合はクエリしない
 | 
			
		||||
@@ -134,6 +136,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
				this.notificationService.readAllNotification(me.id);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const noteIds = notifications
 | 
			
		||||
				.filter((notification): notification is FilterUnionByProperty<MiNotification, 'type', 'mention' | 'reply' | 'quote'> => ['mention', 'reply', 'quote'].includes(notification.type))
 | 
			
		||||
				.map(notification => notification.noteId);
 | 
			
		||||
 | 
			
		||||
			if (noteIds.length > 0) {
 | 
			
		||||
				const notes = await this.notesRepository.findBy({ id: In(noteIds) });
 | 
			
		||||
				this.noteReadService.read(me.id, notes);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return await this.notificationEntityService.packMany(notifications, me.id);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import type { NoteUnreadsRepository } from '@/models/_.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	tags: ['account'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
 | 
			
		||||
	kind: 'write:account',
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const paramDef = {
 | 
			
		||||
	type: 'object',
 | 
			
		||||
	properties: {},
 | 
			
		||||
	required: [],
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.noteUnreadsRepository)
 | 
			
		||||
		private noteUnreadsRepository: NoteUnreadsRepository,
 | 
			
		||||
 | 
			
		||||
		private globalEventService: GlobalEventService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			// Remove documents
 | 
			
		||||
			await this.noteUnreadsRepository.delete({
 | 
			
		||||
				userId: me.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// 全て既読になったイベントを発行
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'readAllUnreadMentions');
 | 
			
		||||
			this.globalEventService.publishMainStream(me.id, 'readAllUnreadSpecifiedNotes');
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -190,7 +190,6 @@ export const paramDef = {
 | 
			
		||||
		autoSensitive: { type: 'boolean' },
 | 
			
		||||
		followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
 | 
			
		||||
		followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
 | 
			
		||||
		chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] },
 | 
			
		||||
		pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
			
		||||
		mutedWords: muteWords,
 | 
			
		||||
		hardMutedWords: muteWords,
 | 
			
		||||
@@ -289,7 +288,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
			if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
 | 
			
		||||
			if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
 | 
			
		||||
			if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
 | 
			
		||||
			if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
 | 
			
		||||
 | 
			
		||||
			function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
 | 
			
		||||
				// TODO: ちゃんと数える
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ export const meta = {
 | 
			
		||||
	tags: ['meta'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canInvite',
 | 
			
		||||
	requireRolePolicy: 'canInvite',
 | 
			
		||||
	kind: 'write:invite-codes',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ export const meta = {
 | 
			
		||||
	tags: ['meta'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canInvite',
 | 
			
		||||
	requireRolePolicy: 'canInvite',
 | 
			
		||||
	kind: 'write:invite-codes',
 | 
			
		||||
 | 
			
		||||
	errors: {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ export const meta = {
 | 
			
		||||
	tags: ['meta'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canInvite',
 | 
			
		||||
	requireRolePolicy: 'canInvite',
 | 
			
		||||
	kind: 'read:invite-codes',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ export const meta = {
 | 
			
		||||
	tags: ['meta'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canInvite',
 | 
			
		||||
	requireRolePolicy: 'canInvite',
 | 
			
		||||
	kind: 'read:invite-codes',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import type { NotesRepository, FollowingsRepository } from '@/models/_.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { QueryService } from '@/core/QueryService.js';
 | 
			
		||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
@@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
 | 
			
		||||
		private noteEntityService: NoteEntityService,
 | 
			
		||||
		private queryService: QueryService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			const followingQuery = this.followingsRepository.createQueryBuilder('following')
 | 
			
		||||
@@ -87,6 +89,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
 | 
			
		||||
			const mentions = await query.limit(ps.limit).getMany();
 | 
			
		||||
 | 
			
		||||
			this.noteReadService.read(me.id, mentions);
 | 
			
		||||
 | 
			
		||||
			return await this.noteEntityService.packMany(mentions, me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import type { NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { GetterService } from '@/server/api/GetterService.js';
 | 
			
		||||
import { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { ApiError } from '../../../error.js';
 | 
			
		||||
 | 
			
		||||
@@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
		private noteThreadMutingsRepository: NoteThreadMutingsRepository,
 | 
			
		||||
 | 
			
		||||
		private getterService: GetterService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
@@ -67,6 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		||||
				}],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			await this.noteReadService.read(me.id, mutedNotes);
 | 
			
		||||
 | 
			
		||||
			await this.noteThreadMutingsRepository.insert({
 | 
			
		||||
				id: this.idService.gen(),
 | 
			
		||||
				threadId: note.threadId ?? note.id,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ export const meta = {
 | 
			
		||||
	tags: ['admin'],
 | 
			
		||||
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requiredRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	requireRolePolicy: 'canManageCustomEmojis',
 | 
			
		||||
	kind: 'read:admin:emoji',
 | 
			
		||||
 | 
			
		||||
	res: {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,6 @@ import { AntennaChannelService } from './channels/antenna.js';
 | 
			
		||||
import { DriveChannelService } from './channels/drive.js';
 | 
			
		||||
import { HashtagChannelService } from './channels/hashtag.js';
 | 
			
		||||
import { RoleTimelineChannelService } from './channels/role-timeline.js';
 | 
			
		||||
import { ChatUserChannelService } from './channels/chat-user.js';
 | 
			
		||||
import { ChatRoomChannelService } from './channels/chat-room.js';
 | 
			
		||||
import { ReversiChannelService } from './channels/reversi.js';
 | 
			
		||||
import { ReversiGameChannelService } from './channels/reversi-game.js';
 | 
			
		||||
import { type MiChannelService } from './channel.js';
 | 
			
		||||
@@ -42,8 +40,6 @@ export class ChannelsService {
 | 
			
		||||
		private serverStatsChannelService: ServerStatsChannelService,
 | 
			
		||||
		private queueStatsChannelService: QueueStatsChannelService,
 | 
			
		||||
		private adminChannelService: AdminChannelService,
 | 
			
		||||
		private chatUserChannelService: ChatUserChannelService,
 | 
			
		||||
		private chatRoomChannelService: ChatRoomChannelService,
 | 
			
		||||
		private reversiChannelService: ReversiChannelService,
 | 
			
		||||
		private reversiGameChannelService: ReversiGameChannelService,
 | 
			
		||||
	) {
 | 
			
		||||
@@ -66,8 +62,6 @@ export class ChannelsService {
 | 
			
		||||
			case 'serverStats': return this.serverStatsChannelService;
 | 
			
		||||
			case 'queueStats': return this.queueStatsChannelService;
 | 
			
		||||
			case 'admin': return this.adminChannelService;
 | 
			
		||||
			case 'chatUser': return this.chatUserChannelService;
 | 
			
		||||
			case 'chatRoom': return this.chatRoomChannelService;
 | 
			
		||||
			case 'reversi': return this.reversiChannelService;
 | 
			
		||||
			case 'reversiGame': return this.reversiGameChannelService;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import * as WebSocket from 'ws';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import type { MiAccessToken } from '@/models/AccessToken.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import type { NoteReadService } from '@/core/NoteReadService.js';
 | 
			
		||||
import type { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
@@ -44,6 +45,7 @@ export default class Connection {
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private channelsService: ChannelsService,
 | 
			
		||||
		private noteReadService: NoteReadService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private cacheService: CacheService,
 | 
			
		||||
		private channelFollowingService: ChannelFollowingService,
 | 
			
		||||
@@ -117,7 +119,7 @@ export default class Connection {
 | 
			
		||||
			case 'readNotification': this.onReadNotification(body); break;
 | 
			
		||||
			case 'subNote': this.onSubscribeNote(body); break;
 | 
			
		||||
			case 's': this.onSubscribeNote(body); break; // alias
 | 
			
		||||
			case 'sr': this.onSubscribeNote(body); break;
 | 
			
		||||
			case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
 | 
			
		||||
			case 'unsubNote': this.onUnsubscribeNote(body); break;
 | 
			
		||||
			case 'un': this.onUnsubscribeNote(body); break; // alias
 | 
			
		||||
			case 'connect': this.onChannelConnectRequested(body); break;
 | 
			
		||||
@@ -152,6 +154,19 @@ export default class Connection {
 | 
			
		||||
		if (note.renote) add(note.renote);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private readNote(body: JsonValue | undefined) {
 | 
			
		||||
		if (!isJsonObject(body)) return;
 | 
			
		||||
		const id = body.id;
 | 
			
		||||
 | 
			
		||||
		const note = this.cachedNotes.find(n => n.id === id);
 | 
			
		||||
		if (note == null) return;
 | 
			
		||||
 | 
			
		||||
		if (this.user && (note.userId !== this.user.id)) {
 | 
			
		||||
			this.noteReadService.read(this.user.id, [note]);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private onReadNotification(payload: JsonValue | undefined) {
 | 
			
		||||
		this.notificationService.readAllNotification(this.user!.id);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { JsonObject } from '@/misc/json-value.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import Channel, { type MiChannelService } from '../channel.js';
 | 
			
		||||
 | 
			
		||||
class ChatRoomChannel extends Channel {
 | 
			
		||||
	public readonly chName = 'chatRoom';
 | 
			
		||||
	public static shouldShare = false;
 | 
			
		||||
	public static requireCredential = true as const;
 | 
			
		||||
	public static kind = 'read:chat';
 | 
			
		||||
	private roomId: string;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
 | 
			
		||||
		id: string,
 | 
			
		||||
		connection: Channel['connection'],
 | 
			
		||||
	) {
 | 
			
		||||
		super(id, connection);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async init(params: JsonObject) {
 | 
			
		||||
		if (typeof params.roomId !== 'string') return;
 | 
			
		||||
		this.roomId = params.roomId;
 | 
			
		||||
 | 
			
		||||
		this.subscriber.on(`chatRoomStream:${this.roomId}`, this.onEvent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async onEvent(data: GlobalEvents['chat']['payload']) {
 | 
			
		||||
		this.send(data.type, data.body);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public onMessage(type: string, body: any) {
 | 
			
		||||
		switch (type) {
 | 
			
		||||
			case 'read':
 | 
			
		||||
				if (this.roomId) {
 | 
			
		||||
					this.chatService.readRoomChatMessage(this.user!.id, this.roomId);
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose() {
 | 
			
		||||
		this.subscriber.off(`chatRoomStream:${this.roomId}`, this.onEvent);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ChatRoomChannelService implements MiChannelService<true> {
 | 
			
		||||
	public readonly shouldShare = ChatRoomChannel.shouldShare;
 | 
			
		||||
	public readonly requireCredential = ChatRoomChannel.requireCredential;
 | 
			
		||||
	public readonly kind = ChatRoomChannel.kind;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public create(id: string, connection: Channel['connection']): ChatRoomChannel {
 | 
			
		||||
		return new ChatRoomChannel(
 | 
			
		||||
			this.chatService,
 | 
			
		||||
			id,
 | 
			
		||||
			connection,
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
 | 
			
		||||
import type { JsonObject } from '@/misc/json-value.js';
 | 
			
		||||
import { ChatService } from '@/core/ChatService.js';
 | 
			
		||||
import Channel, { type MiChannelService } from '../channel.js';
 | 
			
		||||
 | 
			
		||||
class ChatUserChannel extends Channel {
 | 
			
		||||
	public readonly chName = 'chatUser';
 | 
			
		||||
	public static shouldShare = false;
 | 
			
		||||
	public static requireCredential = true as const;
 | 
			
		||||
	public static kind = 'read:chat';
 | 
			
		||||
	private otherId: string;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
 | 
			
		||||
		id: string,
 | 
			
		||||
		connection: Channel['connection'],
 | 
			
		||||
	) {
 | 
			
		||||
		super(id, connection);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async init(params: JsonObject) {
 | 
			
		||||
		if (typeof params.otherId !== 'string') return;
 | 
			
		||||
		this.otherId = params.otherId;
 | 
			
		||||
 | 
			
		||||
		this.subscriber.on(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async onEvent(data: GlobalEvents['chat']['payload']) {
 | 
			
		||||
		this.send(data.type, data.body);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public onMessage(type: string, body: any) {
 | 
			
		||||
		switch (type) {
 | 
			
		||||
			case 'read':
 | 
			
		||||
				if (this.otherId) {
 | 
			
		||||
					this.chatService.readUserChatMessage(this.user!.id, this.otherId);
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose() {
 | 
			
		||||
		this.subscriber.off(`chatUserStream:${this.user!.id}-${this.otherId}`, this.onEvent);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ChatUserChannelService implements MiChannelService<true> {
 | 
			
		||||
	public readonly shouldShare = ChatUserChannel.shouldShare;
 | 
			
		||||
	public readonly requireCredential = ChatUserChannel.requireCredential;
 | 
			
		||||
	public readonly kind = ChatUserChannel.kind;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private chatService: ChatService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public create(id: string, connection: Channel['connection']): ChatUserChannel {
 | 
			
		||||
		return new ChatUserChannel(
 | 
			
		||||
			this.chatService,
 | 
			
		||||
			id,
 | 
			
		||||
			connection,
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user