merge: upstream
This commit is contained in:
		
						commit
						7552cea69a
					
				
					 413 changed files with 5517 additions and 2309 deletions
				
			
		| 
						 | 
				
			
			@ -2,3 +2,4 @@
 | 
			
		|||
POSTGRES_PASSWORD=example-misskey-pass
 | 
			
		||||
POSTGRES_USER=example-misskey-user
 | 
			
		||||
POSTGRES_DB=misskey
 | 
			
		||||
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,12 @@ on:
 | 
			
		|||
    paths:
 | 
			
		||||
      - packages/**
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - packages/frontend/**
 | 
			
		||||
      - packages/sw/**
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - packages/shared/.eslintrc.js
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  pnpm_install:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -41,6 +41,7 @@ docker-compose.yml
 | 
			
		|||
# misskey
 | 
			
		||||
/build
 | 
			
		||||
built
 | 
			
		||||
built-test
 | 
			
		||||
/data
 | 
			
		||||
/.cache-loader
 | 
			
		||||
/db
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,16 +1,22 @@
 | 
			
		|||
<!--
 | 
			
		||||
## 2023.x.x (unreleased)
 | 
			
		||||
## 202x.x.x (Unreleased)
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
-
 | 
			
		||||
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
 | 
			
		||||
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Enhance: Adjusted styling to be closer to Firefish
 | 
			
		||||
- Feat: 新しいゲームを追加
 | 
			
		||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
 | 
			
		||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
 | 
			
		||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
 | 
			
		||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
 | 
			
		||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
-
 | 
			
		||||
 | 
			
		||||
-->
 | 
			
		||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
 | 
			
		||||
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
 | 
			
		||||
- Enhance: クリップをエクスポートできるように
 | 
			
		||||
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
 | 
			
		||||
 | 
			
		||||
## 2023.12.2
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								COPYING
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								COPYING
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
Unless otherwise stated this repository is
 | 
			
		||||
Copyright © 2014-2023 syuilo and contributers
 | 
			
		||||
Copyright © 2014-2024 syuilo and contributors
 | 
			
		||||
 | 
			
		||||
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ Also, the later tasks are more indefinite and are subject to change as developme
 | 
			
		|||
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
 | 
			
		||||
 | 
			
		||||
- ~~Make the number of type errors zero (backend)~~ → Done ✔️
 | 
			
		||||
- Make the number of type errors zero (frontend)
 | 
			
		||||
- Improve CI
 | 
			
		||||
	- ~~Fix tests~~ → Done ✔️
 | 
			
		||||
	- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ services:
 | 
			
		|||
    links:
 | 
			
		||||
      - db
 | 
			
		||||
      - redis
 | 
			
		||||
#     - mcaptcha
 | 
			
		||||
#     - meilisearch
 | 
			
		||||
    depends_on:
 | 
			
		||||
      db:
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,36 @@ services:
 | 
			
		|||
      interval: 5s
 | 
			
		||||
      retries: 20
 | 
			
		||||
 | 
			
		||||
#  mcaptcha:
 | 
			
		||||
#    restart: always
 | 
			
		||||
#    image: mcaptcha/mcaptcha:latest
 | 
			
		||||
#    networks:
 | 
			
		||||
#      internal_network:
 | 
			
		||||
#      external_network:
 | 
			
		||||
#        aliases:
 | 
			
		||||
#          - localhost
 | 
			
		||||
#    ports:
 | 
			
		||||
#      - 7493:7493
 | 
			
		||||
#    env_file:
 | 
			
		||||
#      - .config/docker.env
 | 
			
		||||
#    environment:
 | 
			
		||||
#      PORT: 7493
 | 
			
		||||
#      MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
 | 
			
		||||
#    depends_on:
 | 
			
		||||
#      db:
 | 
			
		||||
#        condition: service_healthy
 | 
			
		||||
#      mcaptcha_redis:
 | 
			
		||||
#        condition: service_healthy
 | 
			
		||||
#
 | 
			
		||||
#  mcaptcha_redis:
 | 
			
		||||
#    image: mcaptcha/cache:latest
 | 
			
		||||
#    networks:
 | 
			
		||||
#      - internal_network
 | 
			
		||||
#    healthcheck:
 | 
			
		||||
#      test: "redis-cli ping"
 | 
			
		||||
#      interval: 5s
 | 
			
		||||
#      retries: 20
 | 
			
		||||
 | 
			
		||||
#  meilisearch:
 | 
			
		||||
#    restart: always
 | 
			
		||||
#    image: getmeili/meilisearch:v1.3.4
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -127,7 +127,7 @@ reaction: "Reactions"
 | 
			
		|||
reactions: "Reactions"
 | 
			
		||||
emojiPicker: "Emoji picker"
 | 
			
		||||
pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
 | 
			
		||||
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when entering emojis"
 | 
			
		||||
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker"
 | 
			
		||||
emojiPickerDisplay: "Emoji picker display"
 | 
			
		||||
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
 | 
			
		||||
overwriteFromPinnedEmojis: "Override from general settings"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										17
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -393,6 +393,11 @@ export interface Locale {
 | 
			
		|||
    "enableHcaptcha": string;
 | 
			
		||||
    "hcaptchaSiteKey": string;
 | 
			
		||||
    "hcaptchaSecretKey": string;
 | 
			
		||||
    "mcaptcha": string;
 | 
			
		||||
    "enableMcaptcha": string;
 | 
			
		||||
    "mcaptchaSiteKey": string;
 | 
			
		||||
    "mcaptchaSecretKey": string;
 | 
			
		||||
    "mcaptchaInstanceUrl": string;
 | 
			
		||||
    "recaptcha": string;
 | 
			
		||||
    "enableRecaptcha": string;
 | 
			
		||||
    "recaptchaSiteKey": string;
 | 
			
		||||
| 
						 | 
				
			
			@ -686,6 +691,7 @@ export interface Locale {
 | 
			
		|||
    "other": string;
 | 
			
		||||
    "regenerateLoginToken": string;
 | 
			
		||||
    "regenerateLoginTokenDescription": string;
 | 
			
		||||
    "theKeywordWhenSearchingForCustomEmoji": string;
 | 
			
		||||
    "setMultipleBySeparatingWithSpace": string;
 | 
			
		||||
    "fileIdOrUrl": string;
 | 
			
		||||
    "behavior": string;
 | 
			
		||||
| 
						 | 
				
			
			@ -1226,6 +1232,7 @@ export interface Locale {
 | 
			
		|||
    "decorate": string;
 | 
			
		||||
    "addMfmFunction": string;
 | 
			
		||||
    "enableQuickAddMfmFunction": string;
 | 
			
		||||
    "bubbleGame": string;
 | 
			
		||||
    "_announcement": {
 | 
			
		||||
        "forExistingUsers": string;
 | 
			
		||||
        "forExistingUsersDescription": string;
 | 
			
		||||
| 
						 | 
				
			
			@ -1690,6 +1697,15 @@ export interface Locale {
 | 
			
		|||
                "title": string;
 | 
			
		||||
                "description": string;
 | 
			
		||||
            };
 | 
			
		||||
            "_bubbleGameExplodingHead": {
 | 
			
		||||
                "title": string;
 | 
			
		||||
                "description": string;
 | 
			
		||||
            };
 | 
			
		||||
            "_bubbleGameDoubleExplodingHead": {
 | 
			
		||||
                "title": string;
 | 
			
		||||
                "description": string;
 | 
			
		||||
                "flavor": string;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    "_role": {
 | 
			
		||||
| 
						 | 
				
			
			@ -2302,6 +2318,7 @@ export interface Locale {
 | 
			
		|||
    "_exportOrImport": {
 | 
			
		||||
        "allNotes": string;
 | 
			
		||||
        "favoritedNotes": string;
 | 
			
		||||
        "clips": string;
 | 
			
		||||
        "followingList": string;
 | 
			
		||||
        "muteList": string;
 | 
			
		||||
        "blockingList": string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -390,6 +390,11 @@ hcaptcha: "hCaptcha"
 | 
			
		|||
enableHcaptcha: "hCaptchaを有効にする"
 | 
			
		||||
hcaptchaSiteKey: "サイトキー"
 | 
			
		||||
hcaptchaSecretKey: "シークレットキー"
 | 
			
		||||
mcaptcha: "mCaptcha"
 | 
			
		||||
enableMcaptcha: "mCaptchaを有効にする"
 | 
			
		||||
mcaptchaSiteKey: "サイトキー"
 | 
			
		||||
mcaptchaSecretKey: "シークレットキー"
 | 
			
		||||
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
 | 
			
		||||
recaptcha: "reCAPTCHA"
 | 
			
		||||
enableRecaptcha: "reCAPTCHAを有効にする"
 | 
			
		||||
recaptchaSiteKey: "サイトキー"
 | 
			
		||||
| 
						 | 
				
			
			@ -683,6 +688,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
 | 
			
		|||
other: "その他"
 | 
			
		||||
regenerateLoginToken: "ログイントークンを再生成"
 | 
			
		||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
 | 
			
		||||
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
 | 
			
		||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
 | 
			
		||||
fileIdOrUrl: "ファイルIDまたはURL"
 | 
			
		||||
behavior: "動作"
 | 
			
		||||
| 
						 | 
				
			
			@ -1223,6 +1229,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
 | 
			
		|||
decorate: "デコる"
 | 
			
		||||
addMfmFunction: "装飾を追加"
 | 
			
		||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
 | 
			
		||||
bubbleGame: "バブルゲーム"
 | 
			
		||||
 | 
			
		||||
_announcement:
 | 
			
		||||
  forExistingUsers: "既存ユーザーのみ"
 | 
			
		||||
| 
						 | 
				
			
			@ -1601,6 +1608,13 @@ _achievements:
 | 
			
		|||
    _tutorialCompleted:
 | 
			
		||||
      title: "Sharkey初心者講座 修了証"
 | 
			
		||||
      description: "チュートリアルを完了した"
 | 
			
		||||
    _bubbleGameExplodingHead:
 | 
			
		||||
      title: "🤯"
 | 
			
		||||
      description: "バブルゲームで最も大きいモノを出した"
 | 
			
		||||
    _bubbleGameDoubleExplodingHead:
 | 
			
		||||
      title: "ダブル🤯"
 | 
			
		||||
      description: "バブルゲームで最も大きいモノを2つ同時に出した"
 | 
			
		||||
      flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
 | 
			
		||||
 | 
			
		||||
_role:
 | 
			
		||||
  new: "ロールの作成"
 | 
			
		||||
| 
						 | 
				
			
			@ -2205,6 +2219,7 @@ _profile:
 | 
			
		|||
_exportOrImport:
 | 
			
		||||
  allNotes: "全てのノート"
 | 
			
		||||
  favoritedNotes: "お気に入りにしたノート"
 | 
			
		||||
  clips: "クリップ"
 | 
			
		||||
  followingList: "フォロー"
 | 
			
		||||
  muteList: "ミュート"
 | 
			
		||||
  blockingList: "ブロック"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
	"name": "sharkey",
 | 
			
		||||
	"version": "2024.1.0.beta1",
 | 
			
		||||
	"version": "2024.1.0.beta2",
 | 
			
		||||
	"codename": "shonk",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -160,7 +160,6 @@ module.exports = {
 | 
			
		|||
	testMatch: [
 | 
			
		||||
		"<rootDir>/test/unit/**/*.ts",
 | 
			
		||||
		"<rootDir>/src/**/*.test.ts",
 | 
			
		||||
		"<rootDir>/test/e2e/**/*.ts",
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										15
									
								
								packages/backend/jest.config.e2e.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/backend/jest.config.e2e.cjs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
/*
 | 
			
		||||
* For a detailed explanation regarding each configuration property and type check, visit:
 | 
			
		||||
* https://jestjs.io/docs/en/configuration.html
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const base = require('./jest.config.cjs')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
	...base,
 | 
			
		||||
	globalSetup: "<rootDir>/built-test/entry.js",
 | 
			
		||||
	setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
 | 
			
		||||
	testMatch: [
 | 
			
		||||
		"<rootDir>/test/e2e/**/*.ts",
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										14
									
								
								packages/backend/jest.config.unit.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/backend/jest.config.unit.cjs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
/*
 | 
			
		||||
* For a detailed explanation regarding each configuration property and type check, visit:
 | 
			
		||||
* https://jestjs.io/docs/en/configuration.html
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const base = require('./jest.config.cjs')
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
	...base,
 | 
			
		||||
	testMatch: [
 | 
			
		||||
		"<rootDir>/test/unit/**/*.ts",
 | 
			
		||||
		"<rootDir>/src/**/*.test.ts",
 | 
			
		||||
	],
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class SupportTrueMailApi1703658526000 {
 | 
			
		||||
    name = 'SupportTrueMailApi1703658526000'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
    	  await queryRunner.query(`ALTER TABLE "meta" ADD "truemailInstance" character varying(1024)`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "truemailAuthKey" character varying(1024)`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "enableTruemailApi" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTruemailApi"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailInstance"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "truemailAuthKey"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								packages/backend/migration/1704373210054-support-mcaptcha.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/backend/migration/1704373210054-support-mcaptcha.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class SupportMcaptcha1704373210054 {
 | 
			
		||||
    name = 'SupportMcaptcha1704373210054'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "enableMcaptcha" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSitekey" character varying(1024)`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaSecretKey" character varying(1024)`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" ADD "mcaptchaInstanceUrl" character varying(1024)`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaInstanceUrl"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSecretKey"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mcaptchaSitekey"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableMcaptcha"`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@
 | 
			
		|||
		"revert": "pnpm typeorm migration:revert -d ormconfig.js",
 | 
			
		||||
		"check:connect": "node ./check_connect.js",
 | 
			
		||||
		"build": "swc src -d built -D",
 | 
			
		||||
		"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
 | 
			
		||||
		"watch:swc": "swc src -d built -D -w",
 | 
			
		||||
		"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
 | 
			
		||||
		"watch": "node watch.mjs",
 | 
			
		||||
| 
						 | 
				
			
			@ -21,11 +22,15 @@
 | 
			
		|||
		"typecheck": "pnpm --filter megalodon build && tsc --noEmit",
 | 
			
		||||
		"eslint": "eslint --quiet \"src/**/*.ts\"",
 | 
			
		||||
		"lint": "pnpm typecheck && pnpm eslint",
 | 
			
		||||
		"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
 | 
			
		||||
		"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
 | 
			
		||||
		"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
 | 
			
		||||
		"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
 | 
			
		||||
		"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
 | 
			
		||||
		"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
 | 
			
		||||
		"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
 | 
			
		||||
		"test": "pnpm jest",
 | 
			
		||||
		"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
 | 
			
		||||
		"test-and-coverage": "pnpm jest-and-coverage",
 | 
			
		||||
		"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
 | 
			
		||||
		"generate-api-json": "node ./generate_api_json.js"
 | 
			
		||||
	},
 | 
			
		||||
	"optionalDependencies": {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +77,8 @@
 | 
			
		|||
		"@fastify/multipart": "8.0.0",
 | 
			
		||||
		"@fastify/static": "6.12.0",
 | 
			
		||||
		"@fastify/view": "8.2.0",
 | 
			
		||||
		"@misskey-dev/sharp-read-bmp": "^1.1.1",
 | 
			
		||||
		"@misskey-dev/summaly": "^5.0.3",
 | 
			
		||||
		"@nestjs/common": "10.2.10",
 | 
			
		||||
		"@nestjs/core": "10.2.10",
 | 
			
		||||
		"@nestjs/testing": "10.2.10",
 | 
			
		||||
| 
						 | 
				
			
			@ -158,11 +165,9 @@
 | 
			
		|||
		"sanitize-html": "2.11.0",
 | 
			
		||||
		"secure-json-parse": "2.7.0",
 | 
			
		||||
		"sharp": "0.32.6",
 | 
			
		||||
		"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
 | 
			
		||||
		"slacc": "0.0.10",
 | 
			
		||||
		"strict-event-emitter-types": "2.0.0",
 | 
			
		||||
		"stringz": "2.1.0",
 | 
			
		||||
		"summaly": "github:misskey-dev/summaly",
 | 
			
		||||
		"systeminformation": "5.21.20",
 | 
			
		||||
		"tinycolor2": "1.6.0",
 | 
			
		||||
		"tmp": "0.2.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -179,6 +184,8 @@
 | 
			
		|||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@jest/globals": "29.7.0",
 | 
			
		||||
		"@misskey-dev/eslint-plugin": "^1.0.0",
 | 
			
		||||
		"@nestjs/platform-express": "^10.3.0",
 | 
			
		||||
		"@simplewebauthn/typescript-types": "8.3.4",
 | 
			
		||||
		"@swc/jest": "0.2.29",
 | 
			
		||||
		"@types/accepts": "1.3.7",
 | 
			
		||||
| 
						 | 
				
			
			@ -228,9 +235,11 @@
 | 
			
		|||
		"eslint": "8.56.0",
 | 
			
		||||
		"eslint-plugin-import": "2.29.1",
 | 
			
		||||
		"execa": "8.0.1",
 | 
			
		||||
		"fkill": "^9.0.0",
 | 
			
		||||
		"jest": "29.7.0",
 | 
			
		||||
		"jest-mock": "29.7.0",
 | 
			
		||||
		"nodemon": "3.0.2",
 | 
			
		||||
		"pid-port": "^1.0.0",
 | 
			
		||||
		"simple-oauth2": "5.0.0"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,6 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setTimeout } from 'node:timers/promises';
 | 
			
		||||
import { Global, Inject, Module } from '@nestjs/common';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
| 
						 | 
				
			
			@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
 | 
			
		|||
import { Config, loadConfig } from './config.js';
 | 
			
		||||
import { createPostgresDataSource } from './postgres.js';
 | 
			
		||||
import { RepositoryModule } from './models/RepositoryModule.js';
 | 
			
		||||
import { allSettled } from './misc/promise-tracker.js';
 | 
			
		||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
const $config: Provider = {
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ const $meilisearch: Provider = {
 | 
			
		|||
	useFactory: (config: Config) => {
 | 
			
		||||
		if (config.meilisearch) {
 | 
			
		||||
			return new MeiliSearch({
 | 
			
		||||
				host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
 | 
			
		||||
				host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
 | 
			
		||||
				apiKey: config.meilisearch.apiKey,
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
 | 
			
		|||
		@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
 | 
			
		||||
		@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
 | 
			
		||||
		@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
 | 
			
		||||
	) {}
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
	public async dispose(): Promise<void> {
 | 
			
		||||
		if (process.env.NODE_ENV === 'test') {
 | 
			
		||||
			// XXX:
 | 
			
		||||
			// Shutting down the existing connections causes errors on Jest as
 | 
			
		||||
			// Misskey has asynchronous postgres/redis connections that are not
 | 
			
		||||
			// awaited.
 | 
			
		||||
			// Let's wait for some random time for them to finish.
 | 
			
		||||
			await setTimeout(5000);
 | 
			
		||||
		}
 | 
			
		||||
		// Wait for all potential DB queries
 | 
			
		||||
		await allSettled();
 | 
			
		||||
		// And then disconnect from DB
 | 
			
		||||
		await Promise.all([
 | 
			
		||||
			this.db.destroy(),
 | 
			
		||||
			this.redisClient.disconnect(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
 | 
			
		|||
	'brainDiver',
 | 
			
		||||
	'smashTestNotificationButton',
 | 
			
		||||
	'tutorialCompleted',
 | 
			
		||||
	'bubbleGameExplodingHead',
 | 
			
		||||
	'bubbleGameDoubleExplodingHead',
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,6 +73,37 @@ export class CaptchaService {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// https://codeberg.org/Gusted/mCaptcha/src/branch/main/mcaptcha.go
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
 | 
			
		||||
		if (response == null) {
 | 
			
		||||
			throw new Error('mcaptcha-failed: no response provided');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
 | 
			
		||||
		const result = await this.httpRequestService.send(endpointUrl.toString(), {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({
 | 
			
		||||
				key: siteKey,
 | 
			
		||||
				secret: secret,
 | 
			
		||||
				token: response,
 | 
			
		||||
			}),
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (result.status !== 200) {
 | 
			
		||||
			throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const resp = (await result.json()) as { valid: boolean };
 | 
			
		||||
 | 
			
		||||
		if (!resp.valid) {
 | 
			
		||||
			throw new Error('mcaptcha-request-failed');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
 | 
			
		||||
		if (response == null) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
 | 
			
		|||
import * as fs from 'node:fs';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import sharp from 'sharp';
 | 
			
		||||
import { sharpBmp } from 'sharp-read-bmp';
 | 
			
		||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -634,7 +634,7 @@ export class DriveService {
 | 
			
		|||
	public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
 | 
			
		||||
		const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
 | 
			
		||||
 | 
			
		||||
		if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
 | 
			
		||||
		if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
 | 
			
		||||
			throw new DriveService.InvalidFileNameError();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -156,7 +156,7 @@ export class EmailService {
 | 
			
		|||
	@bindThis
 | 
			
		||||
	public async validateEmailForAccount(emailAddress: string): Promise<{
 | 
			
		||||
		available: boolean;
 | 
			
		||||
		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned';
 | 
			
		||||
		reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
 | 
			
		||||
	}> {
 | 
			
		||||
		const meta = await this.metaService.fetch();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -173,6 +173,8 @@ export class EmailService {
 | 
			
		|||
		if (meta.enableActiveEmailValidation) {
 | 
			
		||||
			if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
 | 
			
		||||
				validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
 | 
			
		||||
			} else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
 | 
			
		||||
				validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
 | 
			
		||||
			} else {
 | 
			
		||||
				validated = await validateEmail({
 | 
			
		||||
					email: emailAddress,
 | 
			
		||||
| 
						 | 
				
			
			@ -201,6 +203,8 @@ export class EmailService {
 | 
			
		|||
			validated.reason === 'disposable' ? 'disposable' :
 | 
			
		||||
			validated.reason === 'mx' ? 'mx' :
 | 
			
		||||
			validated.reason === 'smtp' ? 'smtp' :
 | 
			
		||||
			validated.reason === 'network' ? 'network' :
 | 
			
		||||
			validated.reason === 'blacklist' ? 'blacklist' :
 | 
			
		||||
			null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -265,4 +269,67 @@ export class EmailService {
 | 
			
		|||
			reason: null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async trueMail<T>(truemailInstance: string, emailAddress: string, truemailAuthKey: string): Promise<{
 | 
			
		||||
		valid: boolean;
 | 
			
		||||
		reason: 'used' | 'format' | 'blacklist' | 'mx' | 'smtp' | 'network' | T | null;
 | 
			
		||||
	}> {
 | 
			
		||||
		const endpoint = truemailInstance + '?email=' + emailAddress;
 | 
			
		||||
		try {
 | 
			
		||||
			const res = await this.httpRequestService.send(endpoint, {
 | 
			
		||||
				method: 'POST',
 | 
			
		||||
				headers: {
 | 
			
		||||
					'Content-Type': 'application/json',
 | 
			
		||||
					Accept: 'application/json',
 | 
			
		||||
					Authorization: truemailAuthKey
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
			
 | 
			
		||||
			const json = (await res.json()) as {
 | 
			
		||||
				email: string;
 | 
			
		||||
				success: boolean;
 | 
			
		||||
				errors?: { 
 | 
			
		||||
					list_match?: string;
 | 
			
		||||
					regex?: string;
 | 
			
		||||
					mx?: string;
 | 
			
		||||
					smtp?: string;
 | 
			
		||||
				} | null;
 | 
			
		||||
			};
 | 
			
		||||
			
 | 
			
		||||
			if (json.email === undefined || (json.email !== undefined && json.errors?.regex)) {
 | 
			
		||||
				return {
 | 
			
		||||
						valid: false,
 | 
			
		||||
						reason: 'format',
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
			if (json.errors?.smtp) {
 | 
			
		||||
				return {
 | 
			
		||||
					valid: false,
 | 
			
		||||
					reason: 'smtp',
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
			if (json.errors?.mx) {
 | 
			
		||||
				return {
 | 
			
		||||
					valid: false,
 | 
			
		||||
					reason: 'mx',
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
			if (!json.success) {
 | 
			
		||||
				return {
 | 
			
		||||
					valid: false,
 | 
			
		||||
					reason: json.errors?.list_match as T || 'blacklist',
 | 
			
		||||
				};
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			return {
 | 
			
		||||
				valid: true,
 | 
			
		||||
				reason: null,
 | 
			
		||||
			};
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			return {
 | 
			
		||||
				valid: false,
 | 
			
		||||
				reason: 'network',
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 | 
			
		|||
import { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
 | 
			
		||||
import { isReply } from '@/misc/is-reply.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
 | 
			
		||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -862,7 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
						this.relayService.deliverToRelays(user, noteActivity);
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					dm.execute();
 | 
			
		||||
					trackPromise(dm.execute());
 | 
			
		||||
				})();
 | 
			
		||||
			}
 | 
			
		||||
			//#endregion
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ 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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
 | 
			
		|||
 | 
			
		||||
			// TODO: ↓まとめてクエリしたい
 | 
			
		||||
 | 
			
		||||
			this.noteUnreadsRepository.countBy({
 | 
			
		||||
			trackPromise(this.noteUnreadsRepository.countBy({
 | 
			
		||||
				userId: userId,
 | 
			
		||||
				isMentioned: true,
 | 
			
		||||
			}).then(mentionsCount => {
 | 
			
		||||
| 
						 | 
				
			
			@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
 | 
			
		|||
					// 全て既読になったイベントを発行
 | 
			
		||||
					this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			}));
 | 
			
		||||
 | 
			
		||||
			this.noteUnreadsRepository.countBy({
 | 
			
		||||
			trackPromise(this.noteUnreadsRepository.countBy({
 | 
			
		||||
				userId: userId,
 | 
			
		||||
				isSpecified: true,
 | 
			
		||||
			}).then(specifiedCount => {
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
 | 
			
		|||
					// 全て既読になったイベントを発行
 | 
			
		||||
					this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			}));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
 | 
			
		|||
import type { Config } from '@/config.js';
 | 
			
		||||
import { UserListService } from '@/core/UserListService.js';
 | 
			
		||||
import type { FilterUnionByProperty } from '@/types.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class NotificationService implements OnApplicationShutdown {
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async createNotification<T extends MiNotification['type']>(
 | 
			
		||||
	public createNotification<T extends MiNotification['type']>(
 | 
			
		||||
		notifieeId: MiUser['id'],
 | 
			
		||||
		type: T,
 | 
			
		||||
		data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
 | 
			
		||||
		notifierId?: MiUser['id'] | null,
 | 
			
		||||
	) {
 | 
			
		||||
		trackPromise(
 | 
			
		||||
			this.#createNotificationInternal(notifieeId, type, data, notifierId),
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async #createNotificationInternal<T extends MiNotification['type']>(
 | 
			
		||||
		notifieeId: MiUser['id'],
 | 
			
		||||
		type: T,
 | 
			
		||||
		data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,12 +3,12 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { setTimeout } from 'node:timers/promises';
 | 
			
		||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import * as Bull from 'bullmq';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
 | 
			
		||||
import { allSettled } from '@/misc/promise-tracker.js';
 | 
			
		||||
import type { Provider } from '@nestjs/common';
 | 
			
		||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
 | 
			
		|||
	) {}
 | 
			
		||||
 | 
			
		||||
	public async dispose(): Promise<void> {
 | 
			
		||||
		if (process.env.NODE_ENV === 'test') {
 | 
			
		||||
			// XXX:
 | 
			
		||||
			// Shutting down the existing connections causes errors on Jest as
 | 
			
		||||
			// Misskey has asynchronous postgres/redis connections that are not
 | 
			
		||||
			// awaited.
 | 
			
		||||
			// Let's wait for some random time for them to finish.
 | 
			
		||||
			await setTimeout(5000);
 | 
			
		||||
		}
 | 
			
		||||
		// Wait for all potential queue jobs
 | 
			
		||||
		await allSettled();
 | 
			
		||||
		// And then close all queues
 | 
			
		||||
		await Promise.all([
 | 
			
		||||
			this.systemQueue.close(),
 | 
			
		||||
			this.endedPollNotificationQueue.close(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '.
 | 
			
		|||
import type httpSignature from '@peertube/http-signature';
 | 
			
		||||
import type * as Bull from 'bullmq';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class QueueService {
 | 
			
		||||
| 
						 | 
				
			
			@ -75,11 +76,15 @@ export class QueueService {
 | 
			
		|||
		if (content == null) return null;
 | 
			
		||||
		if (to == null) return null;
 | 
			
		||||
 | 
			
		||||
		const contentBody = JSON.stringify(content);
 | 
			
		||||
		const digest = ApRequestCreator.createDigest(contentBody);
 | 
			
		||||
 | 
			
		||||
		const data: DeliverJobData = {
 | 
			
		||||
			user: {
 | 
			
		||||
				id: user.id,
 | 
			
		||||
			},
 | 
			
		||||
			content,
 | 
			
		||||
			content: contentBody,
 | 
			
		||||
			digest,
 | 
			
		||||
			to,
 | 
			
		||||
			isSharedInbox,
 | 
			
		||||
		};
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +109,8 @@ export class QueueService {
 | 
			
		|||
	@bindThis
 | 
			
		||||
	public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
 | 
			
		||||
		if (content == null) return null;
 | 
			
		||||
		const contentBody = JSON.stringify(content);
 | 
			
		||||
		const digest = ApRequestCreator.createDigest(contentBody);
 | 
			
		||||
 | 
			
		||||
		const opts = {
 | 
			
		||||
			attempts: this.config.deliverJobMaxAttempts ?? 12,
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +125,8 @@ export class QueueService {
 | 
			
		|||
			name: d[0],
 | 
			
		||||
			data: {
 | 
			
		||||
				user,
 | 
			
		||||
				content,
 | 
			
		||||
				content: contentBody,
 | 
			
		||||
				digest,
 | 
			
		||||
				to: d[0],
 | 
			
		||||
				isSharedInbox: d[1],
 | 
			
		||||
			} as DeliverJobData,
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +193,16 @@ export class QueueService {
 | 
			
		|||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public createExportClipsJob(user: ThinUser) {
 | 
			
		||||
		return this.dbQueue.add('exportClips', {
 | 
			
		||||
			user: { id: user.id },
 | 
			
		||||
		}, {
 | 
			
		||||
			removeOnComplete: true,
 | 
			
		||||
			removeOnFail: true,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public createExportFavoritesJob(user: ThinUser) {
 | 
			
		||||
		return this.dbQueue.add('exportFavorites', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
 | 
			
		|||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import { FeaturedService } from '@/core/FeaturedService.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
 | 
			
		||||
const FALLBACK = '❤';
 | 
			
		||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
 | 
			
		||||
| 
						 | 
				
			
			@ -280,7 +281,7 @@ export class ReactionService {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			dm.execute();
 | 
			
		||||
			trackPromise(dm.execute());
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -328,7 +329,7 @@ export class ReactionService {
 | 
			
		|||
				dm.addDirectRecipe(reactee as MiRemoteUser);
 | 
			
		||||
			}
 | 
			
		||||
			dm.addFollowersRecipe();
 | 
			
		||||
			dm.execute();
 | 
			
		||||
			trackPromise(dm.execute());
 | 
			
		||||
		}
 | 
			
		||||
		//#endregion
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ class DeliverManager {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		// deliver
 | 
			
		||||
		this.queueService.deliverMany(this.actor, this.activity, inboxes);
 | 
			
		||||
		await this.queueService.deliverMany(this.actor, this.activity, inboxes);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -97,6 +97,8 @@ export class ApInboxService {
 | 
			
		|||
				} catch (err) {
 | 
			
		||||
					if (err instanceof Error || typeof err === 'string') {
 | 
			
		||||
						this.logger.error(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						throw err;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +258,7 @@ export class ApInboxService {
 | 
			
		|||
 | 
			
		||||
		const targetUri = getApId(activity.object);
 | 
			
		||||
 | 
			
		||||
		this.announceNote(actor, activity, targetUri);
 | 
			
		||||
		await this.announceNote(actor, activity, targetUri);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			@ -288,7 +290,7 @@ export class ApInboxService {
 | 
			
		|||
			} catch (err) {
 | 
			
		||||
				// 対象が4xxならスキップ
 | 
			
		||||
				if (err instanceof StatusError) {
 | 
			
		||||
					if (err.isClientError) {
 | 
			
		||||
					if (!err.isRetryable) {
 | 
			
		||||
						this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
 | 
			
		||||
						return;
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -373,7 +375,7 @@ export class ApInboxService {
 | 
			
		|||
		});
 | 
			
		||||
 | 
			
		||||
		if (isPost(object)) {
 | 
			
		||||
			this.createNote(resolver, actor, object, false, activity);
 | 
			
		||||
			await this.createNote(resolver, actor, object, false, activity);
 | 
			
		||||
		} else {
 | 
			
		||||
			this.logger.warn(`Unknown type: ${getApType(object)}`);
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -404,7 +406,7 @@ export class ApInboxService {
 | 
			
		|||
			await this.apNoteService.createNote(note, resolver, silent);
 | 
			
		||||
			return 'ok';
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			if (err instanceof StatusError && err.isClientError) {
 | 
			
		||||
			if (err instanceof StatusError && !err.isRetryable) {
 | 
			
		||||
				return `skip ${err.statusCode}`;
 | 
			
		||||
			} else {
 | 
			
		||||
				throw err;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,9 +34,9 @@ type PrivateKey = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export class ApRequestCreator {
 | 
			
		||||
	static createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }): Signed {
 | 
			
		||||
	static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record<string, string> }): Signed {
 | 
			
		||||
		const u = new URL(args.url);
 | 
			
		||||
		const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
 | 
			
		||||
		const digestHeader = args.digest ?? this.createDigest(args.body);
 | 
			
		||||
 | 
			
		||||
		const request: Request = {
 | 
			
		||||
			url: u.href,
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +59,10 @@ export class ApRequestCreator {
 | 
			
		|||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static createDigest(body: string) {
 | 
			
		||||
		return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
 | 
			
		||||
		const u = new URL(args.url);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -145,8 +149,8 @@ export class ApRequestService {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
 | 
			
		||||
		const body = JSON.stringify(object);
 | 
			
		||||
	public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
 | 
			
		||||
		const body = typeof object === 'string' ? object : JSON.stringify(object);
 | 
			
		||||
 | 
			
		||||
		const keypair = await this.userKeypairService.getUserKeypair(user.id);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -157,6 +161,7 @@ export class ApRequestService {
 | 
			
		|||
			},
 | 
			
		||||
			url,
 | 
			
		||||
			body,
 | 
			
		||||
			digest,
 | 
			
		||||
			additionalHeaders: {
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -221,7 +221,7 @@ export class ApNoteService {
 | 
			
		|||
					return { status: 'ok', res };
 | 
			
		||||
				} catch (e) {
 | 
			
		||||
					return {
 | 
			
		||||
						status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror',
 | 
			
		||||
						status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
 | 
			
		||||
					};
 | 
			
		||||
				}
 | 
			
		||||
			};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -369,6 +369,7 @@ export class NoteEntityService implements OnModuleInit {
 | 
			
		|||
				color: channel.color,
 | 
			
		||||
				isSensitive: channel.isSensitive,
 | 
			
		||||
				allowRenoteToExternal: channel.allowRenoteToExternal,
 | 
			
		||||
				userId: channel.userId,
 | 
			
		||||
			} : undefined,
 | 
			
		||||
			mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
 | 
			
		||||
			uri: note.uri ?? undefined,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
 | 
			
		|||
		const log = [] as any[];
 | 
			
		||||
 | 
			
		||||
		ev.on('requestServerStatsLog', x => {
 | 
			
		||||
			ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
 | 
			
		||||
			ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const tick = async () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,8 +71,11 @@ export default class Logger {
 | 
			
		|||
		let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
 | 
			
		||||
		if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
 | 
			
		||||
 | 
			
		||||
		console.log(important ? chalk.bold(log) : log);
 | 
			
		||||
		if (level === 'error' && data) console.log(data);
 | 
			
		||||
		const args: unknown[] = [important ? chalk.bold(log) : log];
 | 
			
		||||
		if (data != null) {
 | 
			
		||||
			args.push(data);
 | 
			
		||||
		}
 | 
			
		||||
		console.log(...args);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								packages/backend/src/misc/promise-tracker.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/backend/src/misc/promise-tracker.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This tracks promises that other modules decided not to wait for,
 | 
			
		||||
 * and makes sure they are all settled before fully closing down the server.
 | 
			
		||||
 */
 | 
			
		||||
export function trackPromise(promise: Promise<unknown>) {
 | 
			
		||||
	if (process.env.NODE_ENV !== 'test') {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	const ref = new WeakRef(promise);
 | 
			
		||||
	promiseRefs.add(ref);
 | 
			
		||||
	promise.finally(() => promiseRefs.delete(ref));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function allSettled(): Promise<void> {
 | 
			
		||||
	await Promise.allSettled([...promiseRefs].map(r => r.deref()));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ export class StatusError extends Error {
 | 
			
		|||
	public statusCode: number;
 | 
			
		||||
	public statusMessage?: string;
 | 
			
		||||
	public isClientError: boolean;
 | 
			
		||||
	public isRetryable: boolean;
 | 
			
		||||
 | 
			
		||||
	constructor(message: string, statusCode: number, statusMessage?: string) {
 | 
			
		||||
		super(message);
 | 
			
		||||
| 
						 | 
				
			
			@ -14,5 +15,6 @@ export class StatusError extends Error {
 | 
			
		|||
		this.statusCode = statusCode;
 | 
			
		||||
		this.statusMessage = statusMessage;
 | 
			
		||||
		this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
 | 
			
		||||
		this.isRetryable = !this.isClientError || this.statusCode === 429;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -196,6 +196,29 @@ export class MiMeta {
 | 
			
		|||
	})
 | 
			
		||||
	public hcaptchaSecretKey: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public enableMcaptcha: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public mcaptchaSitekey: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public mcaptchaSecretKey: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public mcaptchaInstanceUrl: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -467,6 +490,23 @@ export class MiMeta {
 | 
			
		|||
	})
 | 
			
		||||
	public verifymailAuthKey: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public enableTruemailApi: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public truemailInstance: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 1024,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public truemailAuthKey: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: true,
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -148,6 +148,10 @@ export const packedNoteSchema = {
 | 
			
		|||
					type: 'boolean',
 | 
			
		||||
					optional: false, nullable: false,
 | 
			
		||||
				},
 | 
			
		||||
				userId: {
 | 
			
		||||
					type: 'string',
 | 
			
		||||
					optional: false, nullable: true,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		localOnly: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
 | 
			
		|||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
 | 
			
		||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
 | 
			
		||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
 | 
			
		||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
 | 
			
		||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
 | 
			
		||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
 | 
			
		||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +57,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
 | 
			
		|||
		ExportAccountDataProcessorService,
 | 
			
		||||
		ExportCustomEmojisProcessorService,
 | 
			
		||||
		ExportNotesProcessorService,
 | 
			
		||||
		ExportClipsProcessorService,
 | 
			
		||||
		ExportFavoritesProcessorService,
 | 
			
		||||
		ExportFollowingProcessorService,
 | 
			
		||||
		ExportMutingProcessorService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesP
 | 
			
		|||
import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.js';
 | 
			
		||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
 | 
			
		||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
 | 
			
		||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
 | 
			
		||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
 | 
			
		||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
 | 
			
		||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
		private exportAccountDataProcessorService: ExportAccountDataProcessorService,
 | 
			
		||||
		private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
 | 
			
		||||
		private exportNotesProcessorService: ExportNotesProcessorService,
 | 
			
		||||
		private exportClipsProcessorService: ExportClipsProcessorService,
 | 
			
		||||
		private exportFavoritesProcessorService: ExportFavoritesProcessorService,
 | 
			
		||||
		private exportFollowingProcessorService: ExportFollowingProcessorService,
 | 
			
		||||
		private exportMutingProcessorService: ExportMutingProcessorService,
 | 
			
		||||
| 
						 | 
				
			
			@ -169,6 +171,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
 | 
			
		|||
				case 'exportAccountData': return this.exportAccountDataProcessorService.process(job);
 | 
			
		||||
				case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
 | 
			
		||||
				case 'exportNotes': return this.exportNotesProcessorService.process(job);
 | 
			
		||||
				case 'exportClips': return this.exportClipsProcessorService.process(job);
 | 
			
		||||
				case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
 | 
			
		||||
				case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
 | 
			
		||||
				case 'exportMuting': return this.exportMutingProcessorService.process(job);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,7 +72,7 @@ export class DeliverProcessorService {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
 | 
			
		||||
			await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
 | 
			
		||||
 | 
			
		||||
			// Update stats
 | 
			
		||||
			this.federatedInstanceService.fetch(host).then(i => {
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +111,7 @@ export class DeliverProcessorService {
 | 
			
		|||
 | 
			
		||||
			if (res instanceof StatusError) {
 | 
			
		||||
				// 4xx
 | 
			
		||||
				if (res.isClientError) {
 | 
			
		||||
				if (!res.isRetryable) {
 | 
			
		||||
					// 相手が閉鎖していることを明示しているため、配送停止する
 | 
			
		||||
					if (job.data.isSharedInbox && res.statusCode === 410) {
 | 
			
		||||
						this.federatedInstanceService.fetch(host).then(i => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,206 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import { Writable } from 'node:stream';
 | 
			
		||||
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
 | 
			
		||||
import { MoreThan } from 'typeorm';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import type { MiPoll } from '@/models/Poll.js';
 | 
			
		||||
import type { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
 | 
			
		||||
import { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { QueueLoggerService } from '../QueueLoggerService.js';
 | 
			
		||||
import type * as Bull from 'bullmq';
 | 
			
		||||
import type { DbJobDataWithUser } from '../types.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ExportClipsProcessorService {
 | 
			
		||||
	private logger: Logger;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.pollsRepository)
 | 
			
		||||
		private pollsRepository: PollsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.clipsRepository)
 | 
			
		||||
		private clipsRepository: ClipsRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.clipNotesRepository)
 | 
			
		||||
		private clipNotesRepository: ClipNotesRepository,
 | 
			
		||||
 | 
			
		||||
		private driveService: DriveService,
 | 
			
		||||
		private queueLoggerService: QueueLoggerService,
 | 
			
		||||
		private idService: IdService,
 | 
			
		||||
	) {
 | 
			
		||||
		this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
 | 
			
		||||
		this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
 | 
			
		||||
 | 
			
		||||
		const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
 | 
			
		||||
		if (user == null) {
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create temp file
 | 
			
		||||
		const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
		this.logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
 | 
			
		||||
			const writer = stream.getWriter();
 | 
			
		||||
			writer.closed.catch(this.logger.error);
 | 
			
		||||
 | 
			
		||||
			await writer.write('[');
 | 
			
		||||
 | 
			
		||||
			await this.processClips(writer, user, job);
 | 
			
		||||
 | 
			
		||||
			await writer.write(']');
 | 
			
		||||
			await writer.close();
 | 
			
		||||
 | 
			
		||||
			this.logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
			const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
 | 
			
		||||
			const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
 | 
			
		||||
 | 
			
		||||
			this.logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
		} finally {
 | 
			
		||||
			cleanup();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
 | 
			
		||||
		let exportedClipsCount = 0;
 | 
			
		||||
		let cursor: MiClip['id'] | null = null;
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const clips = await this.clipsRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					userId: user.id,
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (clips.length === 0) {
 | 
			
		||||
				job.updateProgress(100);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cursor = clips.at(-1)?.id ?? null;
 | 
			
		||||
 | 
			
		||||
			for (const clip of clips) {
 | 
			
		||||
				// Stringify but remove the last `]}`
 | 
			
		||||
				const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
 | 
			
		||||
				const isFirst = exportedClipsCount === 0;
 | 
			
		||||
				await writer.write(isFirst ? content : ',\n' + content);
 | 
			
		||||
 | 
			
		||||
				await this.processClipNotes(writer, clip.id);
 | 
			
		||||
 | 
			
		||||
				await writer.write(']}');
 | 
			
		||||
				exportedClipsCount++;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const total = await this.clipsRepository.countBy({
 | 
			
		||||
				userId: user.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			job.updateProgress(exportedClipsCount / total);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
 | 
			
		||||
		let exportedClipNotesCount = 0;
 | 
			
		||||
		let cursor: MiClipNote['id'] | null = null;
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const clipNotes = await this.clipNotesRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					clipId,
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
				relations: ['note', 'note.user'],
 | 
			
		||||
			}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
 | 
			
		||||
 | 
			
		||||
			if (clipNotes.length === 0) {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cursor = clipNotes.at(-1)?.id ?? null;
 | 
			
		||||
 | 
			
		||||
			for (const clipNote of clipNotes) {
 | 
			
		||||
				let poll: MiPoll | undefined;
 | 
			
		||||
				if (clipNote.note.hasPoll) {
 | 
			
		||||
					poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
 | 
			
		||||
				}
 | 
			
		||||
				const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
 | 
			
		||||
				const isFirst = exportedClipNotesCount === 0;
 | 
			
		||||
				await writer.write(isFirst ? content : ',\n' + content);
 | 
			
		||||
 | 
			
		||||
				exportedClipNotesCount++;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private serializeClip(clip: MiClip): Record<string, unknown> {
 | 
			
		||||
		return {
 | 
			
		||||
			id: clip.id,
 | 
			
		||||
			name: clip.name,
 | 
			
		||||
			description: clip.description,
 | 
			
		||||
			lastClippedAt: clip.lastClippedAt?.toISOString(),
 | 
			
		||||
			clipNotes: [],
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
 | 
			
		||||
		return {
 | 
			
		||||
			id: clip.id,
 | 
			
		||||
			createdAt: this.idService.parse(clip.id).date.toISOString(),
 | 
			
		||||
			note: {
 | 
			
		||||
				id: clip.note.id,
 | 
			
		||||
				text: clip.note.text,
 | 
			
		||||
				createdAt: this.idService.parse(clip.note.id).date.toISOString(),
 | 
			
		||||
				fileIds: clip.note.fileIds,
 | 
			
		||||
				replyId: clip.note.replyId,
 | 
			
		||||
				renoteId: clip.note.renoteId,
 | 
			
		||||
				poll: poll,
 | 
			
		||||
				cw: clip.note.cw,
 | 
			
		||||
				visibility: clip.note.visibility,
 | 
			
		||||
				visibleUserIds: clip.note.visibleUserIds,
 | 
			
		||||
				localOnly: clip.note.localOnly,
 | 
			
		||||
				reactionAcceptance: clip.note.reactionAcceptance,
 | 
			
		||||
				uri: clip.note.uri,
 | 
			
		||||
				url: clip.note.url,
 | 
			
		||||
				user: {
 | 
			
		||||
					id: clip.note.user.id,
 | 
			
		||||
					name: clip.note.user.name,
 | 
			
		||||
					username: clip.note.user.username,
 | 
			
		||||
					host: clip.note.user.host,
 | 
			
		||||
					uri: clip.note.user.uri,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -85,7 +85,7 @@ export class InboxProcessorService {
 | 
			
		|||
			} catch (err) {
 | 
			
		||||
				// 対象が4xxならスキップ
 | 
			
		||||
				if (err instanceof StatusError) {
 | 
			
		||||
					if (err.isClientError) {
 | 
			
		||||
					if (!err.isRetryable) {
 | 
			
		||||
						throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
 | 
			
		||||
					}
 | 
			
		||||
					throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ export class WebhookDeliverProcessorService {
 | 
			
		|||
 | 
			
		||||
			if (res instanceof StatusError) {
 | 
			
		||||
				// 4xx
 | 
			
		||||
				if (res.isClientError) {
 | 
			
		||||
				if (!res.isRetryable) {
 | 
			
		||||
					throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,9 @@ export type DeliverJobData = {
 | 
			
		|||
	/** Actor */
 | 
			
		||||
	user: ThinUser;
 | 
			
		||||
	/** Activity */
 | 
			
		||||
	content: unknown;
 | 
			
		||||
	content: string;
 | 
			
		||||
	/** Digest header */
 | 
			
		||||
	digest: string;
 | 
			
		||||
	/** inbox URL to deliver */
 | 
			
		||||
	to: string;
 | 
			
		||||
	/** whether it is sharedInbox */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import { dirname } from 'node:path';
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import rename from 'rename';
 | 
			
		||||
import sharp from 'sharp';
 | 
			
		||||
import { sharpBmp } from 'sharp-read-bmp';
 | 
			
		||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -214,6 +214,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
 | 
			
		|||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
 | 
			
		||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
 | 
			
		||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
 | 
			
		||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
 | 
			
		||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
 | 
			
		||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
 | 
			
		||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -588,6 +589,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass:
 | 
			
		|||
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
 | 
			
		||||
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
 | 
			
		||||
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
 | 
			
		||||
const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
 | 
			
		||||
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
 | 
			
		||||
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
 | 
			
		||||
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
 | 
			
		||||
| 
						 | 
				
			
			@ -966,6 +968,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
 | 
			
		|||
		$i_exportFollowing,
 | 
			
		||||
		$i_exportMute,
 | 
			
		||||
		$i_exportNotes,
 | 
			
		||||
		$i_exportClips,
 | 
			
		||||
		$i_exportFavorites,
 | 
			
		||||
		$i_exportUserLists,
 | 
			
		||||
		$i_exportAntennas,
 | 
			
		||||
| 
						 | 
				
			
			@ -1338,6 +1341,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
 | 
			
		|||
		$i_exportFollowing,
 | 
			
		||||
		$i_exportMute,
 | 
			
		||||
		$i_exportNotes,
 | 
			
		||||
		$i_exportClips,
 | 
			
		||||
		$i_exportFavorites,
 | 
			
		||||
		$i_exportUserLists,
 | 
			
		||||
		$i_exportAntennas,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,6 +70,7 @@ export class SignupApiService {
 | 
			
		|||
				'hcaptcha-response'?: string;
 | 
			
		||||
				'g-recaptcha-response'?: string;
 | 
			
		||||
				'turnstile-response'?: string;
 | 
			
		||||
				'm-captcha-response'?: string;
 | 
			
		||||
			}
 | 
			
		||||
		}>,
 | 
			
		||||
		reply: FastifyReply,
 | 
			
		||||
| 
						 | 
				
			
			@ -87,6 +88,12 @@ export class SignupApiService {
 | 
			
		|||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
 | 
			
		||||
				await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
 | 
			
		||||
				await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
 | 
			
		||||
					throw new FastifyReplyError(400, err);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { permissions } from 'misskey-js';
 | 
			
		||||
import type { Schema } from '@/misc/json-schema.js';
 | 
			
		||||
import { permissions } from 'misskey-js';
 | 
			
		||||
import { RolePolicies } from '@/core/RoleService.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -215,6 +216,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
 | 
			
		|||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
 | 
			
		||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
 | 
			
		||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
 | 
			
		||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
 | 
			
		||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
 | 
			
		||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
 | 
			
		||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -587,6 +589,7 @@ const eps = [
 | 
			
		|||
	['i/export-following', ep___i_exportFollowing],
 | 
			
		||||
	['i/export-mute', ep___i_exportMute],
 | 
			
		||||
	['i/export-notes', ep___i_exportNotes],
 | 
			
		||||
	['i/export-clips', ep___i_exportClips],
 | 
			
		||||
	['i/export-favorites', ep___i_exportFavorites],
 | 
			
		||||
	['i/export-user-lists', ep___i_exportUserLists],
 | 
			
		||||
	['i/export-antennas', ep___i_exportAntennas],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,18 @@ export const meta = {
 | 
			
		|||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableMcaptcha: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
			mcaptchaSiteKey: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			mcaptchaInstanceUrl: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableRecaptcha: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -174,6 +186,10 @@ export const meta = {
 | 
			
		|||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			mcaptchaSecretKey: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			recaptchaSecretKey: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
| 
						 | 
				
			
			@ -299,6 +315,18 @@ export const meta = {
 | 
			
		|||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableTruemailApi: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
			truemailInstance: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			truemailAuthKey: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableChartsForRemoteUser: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -476,6 +504,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				approvalRequiredForSignup: instance.approvalRequiredForSignup,
 | 
			
		||||
				enableHcaptcha: instance.enableHcaptcha,
 | 
			
		||||
				hcaptchaSiteKey: instance.hcaptchaSiteKey,
 | 
			
		||||
				enableMcaptcha: instance.enableMcaptcha,
 | 
			
		||||
				mcaptchaSiteKey: instance.mcaptchaSitekey,
 | 
			
		||||
				mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
 | 
			
		||||
				enableRecaptcha: instance.enableRecaptcha,
 | 
			
		||||
				recaptchaSiteKey: instance.recaptchaSiteKey,
 | 
			
		||||
				enableTurnstile: instance.enableTurnstile,
 | 
			
		||||
| 
						 | 
				
			
			@ -508,6 +539,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				preservedUsernames: instance.preservedUsernames,
 | 
			
		||||
				bubbleInstances: instance.bubbleInstances,
 | 
			
		||||
				hcaptchaSecretKey: instance.hcaptchaSecretKey,
 | 
			
		||||
				mcaptchaSecretKey: instance.mcaptchaSecretKey,
 | 
			
		||||
				recaptchaSecretKey: instance.recaptchaSecretKey,
 | 
			
		||||
				turnstileSecretKey: instance.turnstileSecretKey,
 | 
			
		||||
				sensitiveMediaDetection: instance.sensitiveMediaDetection,
 | 
			
		||||
| 
						 | 
				
			
			@ -543,6 +575,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				enableActiveEmailValidation: instance.enableActiveEmailValidation,
 | 
			
		||||
				enableVerifymailApi: instance.enableVerifymailApi,
 | 
			
		||||
				verifymailAuthKey: instance.verifymailAuthKey,
 | 
			
		||||
				enableTruemailApi: instance.enableTruemailApi,
 | 
			
		||||
				truemailInstance: instance.truemailInstance,
 | 
			
		||||
				truemailAuthKey: instance.truemailAuthKey,
 | 
			
		||||
				enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
 | 
			
		||||
				enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
 | 
			
		||||
				enableServerMachineStats: instance.enableServerMachineStats,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,6 +65,10 @@ export const paramDef = {
 | 
			
		|||
		enableHcaptcha: { type: 'boolean' },
 | 
			
		||||
		hcaptchaSiteKey: { type: 'string', nullable: true },
 | 
			
		||||
		hcaptchaSecretKey: { type: 'string', nullable: true },
 | 
			
		||||
		enableMcaptcha: { type: 'boolean' },
 | 
			
		||||
		mcaptchaSiteKey: { type: 'string', nullable: true },
 | 
			
		||||
		mcaptchaInstanceUrl: { type: 'string', nullable: true },
 | 
			
		||||
		mcaptchaSecretKey: { type: 'string', nullable: true },
 | 
			
		||||
		enableRecaptcha: { type: 'boolean' },
 | 
			
		||||
		recaptchaSiteKey: { type: 'string', nullable: true },
 | 
			
		||||
		recaptchaSecretKey: { type: 'string', nullable: true },
 | 
			
		||||
| 
						 | 
				
			
			@ -119,6 +123,9 @@ export const paramDef = {
 | 
			
		|||
		enableActiveEmailValidation: { type: 'boolean' },
 | 
			
		||||
		enableVerifymailApi: { type: 'boolean' },
 | 
			
		||||
		verifymailAuthKey: { type: 'string', nullable: true },
 | 
			
		||||
		enableTruemailApi: { type: 'boolean' },
 | 
			
		||||
		truemailInstance: { type: 'string', nullable: true },
 | 
			
		||||
		truemailAuthKey: { type: 'string', nullable: true },
 | 
			
		||||
		enableChartsForRemoteUser: { type: 'boolean' },
 | 
			
		||||
		enableChartsForFederatedInstances: { type: 'boolean' },
 | 
			
		||||
		enableServerMachineStats: { type: 'boolean' },
 | 
			
		||||
| 
						 | 
				
			
			@ -279,6 +286,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.enableMcaptcha !== undefined) {
 | 
			
		||||
				set.enableMcaptcha = ps.enableMcaptcha;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.mcaptchaSiteKey !== undefined) {
 | 
			
		||||
				set.mcaptchaSitekey = ps.mcaptchaSiteKey;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.mcaptchaInstanceUrl !== undefined) {
 | 
			
		||||
				set.mcaptchaInstanceUrl = ps.mcaptchaInstanceUrl;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.mcaptchaSecretKey !== undefined) {
 | 
			
		||||
				set.mcaptchaSecretKey = ps.mcaptchaSecretKey;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.enableRecaptcha !== undefined) {
 | 
			
		||||
				set.enableRecaptcha = ps.enableRecaptcha;
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -471,6 +494,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.enableTruemailApi !== undefined) {
 | 
			
		||||
				set.enableTruemailApi = ps.enableTruemailApi;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.truemailInstance !== undefined) {
 | 
			
		||||
				if (ps.truemailInstance === '') {
 | 
			
		||||
					set.truemailInstance = null;
 | 
			
		||||
				} else {
 | 
			
		||||
					set.truemailInstance = ps.truemailInstance;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.truemailAuthKey !== undefined) {
 | 
			
		||||
				if (ps.truemailAuthKey === '') {
 | 
			
		||||
					set.truemailAuthKey = null;
 | 
			
		||||
				} else {
 | 
			
		||||
					set.truemailAuthKey = ps.truemailAuthKey;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (ps.enableChartsForRemoteUser !== undefined) {
 | 
			
		||||
				set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
 | 
			
		|||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
 | 
			
		||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
			
		||||
import { trackPromise } from '@/misc/promise-tracker.js';
 | 
			
		||||
import { ApiError } from '../../error.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
 | 
			
		||||
			antenna.isActive = true;
 | 
			
		||||
			antenna.lastUsedAt = new Date();
 | 
			
		||||
			this.antennasRepository.update(antenna.id, antenna);
 | 
			
		||||
			trackPromise(this.antennasRepository.update(antenna.id, antenna));
 | 
			
		||||
 | 
			
		||||
			if (needPublishEvent) {
 | 
			
		||||
				this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										35
									
								
								packages/backend/src/server/api/endpoints/i/export-clips.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/backend/src/server/api/endpoints/i/export-clips.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	secure: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	limit: {
 | 
			
		||||
		duration: ms('1day'),
 | 
			
		||||
		max: 1,
 | 
			
		||||
	},
 | 
			
		||||
} 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(
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
	) {
 | 
			
		||||
		super(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			this.queueService.createExportClipsJob(me);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -115,6 +115,18 @@ export const meta = {
 | 
			
		|||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableMcaptcha: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
			},
 | 
			
		||||
			mcaptchaSiteKey: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			mcaptchaInstanceUrl: {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				optional: false, nullable: true,
 | 
			
		||||
			},
 | 
			
		||||
			enableRecaptcha: {
 | 
			
		||||
				type: 'boolean',
 | 
			
		||||
				optional: false, nullable: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -359,6 +371,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
				approvalRequiredForSignup: instance.approvalRequiredForSignup,
 | 
			
		||||
				enableHcaptcha: instance.enableHcaptcha,
 | 
			
		||||
				hcaptchaSiteKey: instance.hcaptchaSiteKey,
 | 
			
		||||
				enableMcaptcha: instance.enableMcaptcha,
 | 
			
		||||
				mcaptchaSiteKey: instance.mcaptchaSitekey,
 | 
			
		||||
				mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
 | 
			
		||||
				enableRecaptcha: instance.enableRecaptcha,
 | 
			
		||||
				enableAchievements: instance.enableAchievements,
 | 
			
		||||
				recaptchaSiteKey: instance.recaptchaSiteKey,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ class UserListChannel extends Channel {
 | 
			
		|||
	private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
 | 
			
		||||
	private listUsersClock: NodeJS.Timeout;
 | 
			
		||||
	private withFiles: boolean;
 | 
			
		||||
	private withRenotes: boolean;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private userListsRepository: UserListsRepository,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,6 +40,7 @@ class UserListChannel extends Channel {
 | 
			
		|||
	public async init(params: any) {
 | 
			
		||||
		this.listId = params.listId as string;
 | 
			
		||||
		this.withFiles = params.withFiles ?? false;
 | 
			
		||||
		this.withRenotes = params.withRenotes ?? true;
 | 
			
		||||
 | 
			
		||||
		// Check existence and owner
 | 
			
		||||
		const listExist = await this.userListsRepository.exist({
 | 
			
		||||
| 
						 | 
				
			
			@ -104,6 +106,8 @@ class UserListChannel extends Channel {
 | 
			
		|||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
 | 
			
		||||
 | 
			
		||||
		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 | 
			
		||||
		if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
 | 
			
		||||
		// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { summaly } from 'summaly';
 | 
			
		||||
import { summaly } from '@misskey-dev/summaly';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import type { Config } from '@/config.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								packages/backend/test-server/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								packages/backend/test-server/.eslintrc.cjs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
	parserOptions: {
 | 
			
		||||
		tsconfigRootDir: __dirname,
 | 
			
		||||
		project: ['./tsconfig.json'],
 | 
			
		||||
	},
 | 
			
		||||
	extends: [
 | 
			
		||||
		'../../shared/.eslintrc.js',
 | 
			
		||||
	],
 | 
			
		||||
	rules: {
 | 
			
		||||
		'import/order': ['warn', {
 | 
			
		||||
			'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
 | 
			
		||||
			'pathGroups': [
 | 
			
		||||
				{
 | 
			
		||||
					'pattern': '@/**',
 | 
			
		||||
					'group': 'external',
 | 
			
		||||
					'position': 'after'
 | 
			
		||||
				}
 | 
			
		||||
			],
 | 
			
		||||
		}],
 | 
			
		||||
		'no-restricted-globals': [
 | 
			
		||||
			'error',
 | 
			
		||||
			{
 | 
			
		||||
				'name': '__dirname',
 | 
			
		||||
				'message': 'Not in ESModule. Use `import.meta.url` instead.'
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				'name': '__filename',
 | 
			
		||||
				'message': 'Not in ESModule. Use `import.meta.url` instead.'
 | 
			
		||||
			}
 | 
			
		||||
	]
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								packages/backend/test-server/.swcrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packages/backend/test-server/.swcrc
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
{
 | 
			
		||||
	"$schema": "https://json.schemastore.org/swcrc",
 | 
			
		||||
	"jsc": {
 | 
			
		||||
		"parser": {
 | 
			
		||||
			"syntax": "typescript",
 | 
			
		||||
			"dynamicImport": true,
 | 
			
		||||
			"decorators": true
 | 
			
		||||
		},
 | 
			
		||||
		"transform": {
 | 
			
		||||
			"legacyDecorator": true,
 | 
			
		||||
			"decoratorMetadata": true
 | 
			
		||||
		},
 | 
			
		||||
		"experimental": {
 | 
			
		||||
			"keepImportAssertions": true
 | 
			
		||||
		},
 | 
			
		||||
		"baseUrl": "../built",
 | 
			
		||||
		"paths": {
 | 
			
		||||
			"@/*": ["*"]
 | 
			
		||||
		},
 | 
			
		||||
		"target": "es2022"
 | 
			
		||||
	},
 | 
			
		||||
	"minify": false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								packages/backend/test-server/entry.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								packages/backend/test-server/entry.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,80 @@
 | 
			
		|||
import { portToPid } from 'pid-port';
 | 
			
		||||
import fkill from 'fkill';
 | 
			
		||||
import Fastify from 'fastify';
 | 
			
		||||
import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { MainModule } from '@/MainModule.js';
 | 
			
		||||
import { ServerService } from '@/server/ServerService.js';
 | 
			
		||||
import { loadConfig } from '@/config.js';
 | 
			
		||||
import { NestLogger } from '@/NestLogger.js';
 | 
			
		||||
 | 
			
		||||
const config = loadConfig();
 | 
			
		||||
const originEnv = JSON.stringify(process.env);
 | 
			
		||||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * テスト用のサーバインスタンスを起動する
 | 
			
		||||
 */
 | 
			
		||||
async function launch() {
 | 
			
		||||
	await killTestServer();
 | 
			
		||||
 | 
			
		||||
	console.log('starting application...');
 | 
			
		||||
 | 
			
		||||
	const app = await NestFactory.createApplicationContext(MainModule, {
 | 
			
		||||
		logger: new NestLogger(),
 | 
			
		||||
	});
 | 
			
		||||
	const serverService = app.get(ServerService);
 | 
			
		||||
	await serverService.launch();
 | 
			
		||||
 | 
			
		||||
	await startControllerEndpoints();
 | 
			
		||||
 | 
			
		||||
	// ジョブキューは必要な時にテストコード側で起動する
 | 
			
		||||
	// ジョブキューが動くとテスト結果の確認に支障が出ることがあるので意図的に動かさないでいる
 | 
			
		||||
 | 
			
		||||
	console.log('application initialized.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 既に重複したポートで待ち受けしているサーバがある場合はkillする
 | 
			
		||||
 */
 | 
			
		||||
async function killTestServer() {
 | 
			
		||||
	//
 | 
			
		||||
	try {
 | 
			
		||||
		const pid = await portToPid(config.port);
 | 
			
		||||
		if (pid) {
 | 
			
		||||
			await fkill(pid, { force: true });
 | 
			
		||||
		}
 | 
			
		||||
	} catch {
 | 
			
		||||
		// NOP;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 別プロセスに切り離してしまったが故に出来なくなった環境変数の書き換え等を実現するためのエンドポイントを作る
 | 
			
		||||
 * @param port
 | 
			
		||||
 */
 | 
			
		||||
async function startControllerEndpoints(port = config.port + 1000) {
 | 
			
		||||
	const fastify = Fastify();
 | 
			
		||||
 | 
			
		||||
	fastify.post<{ Body: { key?: string, value?: string } }>('/env', async (req, res) => {
 | 
			
		||||
		console.log(req.body);
 | 
			
		||||
		const key = req.body['key'];
 | 
			
		||||
		if (!key) {
 | 
			
		||||
			res.code(400).send({ success: false });
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		process.env[key] = req.body['value'];
 | 
			
		||||
 | 
			
		||||
		res.code(200).send({ success: true });
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => {
 | 
			
		||||
		process.env = JSON.parse(originEnv);
 | 
			
		||||
		res.code(200).send({ success: true });
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await fastify.listen({ port: port, host: 'localhost' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default launch;
 | 
			
		||||
							
								
								
									
										52
									
								
								packages/backend/test-server/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								packages/backend/test-server/tsconfig.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
{
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"allowJs": true,
 | 
			
		||||
		"noEmitOnError": true,
 | 
			
		||||
		"noImplicitAny": true,
 | 
			
		||||
		"noImplicitReturns": true,
 | 
			
		||||
		"noUnusedParameters": false,
 | 
			
		||||
		"noUnusedLocals": false,
 | 
			
		||||
		"noFallthroughCasesInSwitch": true,
 | 
			
		||||
		"declaration": false,
 | 
			
		||||
		"sourceMap": true,
 | 
			
		||||
		"target": "ES2022",
 | 
			
		||||
		"module": "nodenext",
 | 
			
		||||
		"moduleResolution": "nodenext",
 | 
			
		||||
		"allowSyntheticDefaultImports": true,
 | 
			
		||||
		"removeComments": false,
 | 
			
		||||
		"noLib": false,
 | 
			
		||||
		"strict": true,
 | 
			
		||||
		"strictNullChecks": true,
 | 
			
		||||
		"strictPropertyInitialization": false,
 | 
			
		||||
		"skipLibCheck": true,
 | 
			
		||||
		"experimentalDecorators": true,
 | 
			
		||||
		"emitDecoratorMetadata": true,
 | 
			
		||||
		"resolveJsonModule": true,
 | 
			
		||||
		"isolatedModules": true,
 | 
			
		||||
		"rootDir": "../src",
 | 
			
		||||
		"baseUrl": "./",
 | 
			
		||||
		"paths": {
 | 
			
		||||
			"@/*": ["../src/*"]
 | 
			
		||||
		},
 | 
			
		||||
		"outDir": "../built-test",
 | 
			
		||||
		"types": [
 | 
			
		||||
			"node"
 | 
			
		||||
		],
 | 
			
		||||
		"typeRoots": [
 | 
			
		||||
			"../src/@types",
 | 
			
		||||
			"../node_modules/@types",
 | 
			
		||||
			"../node_modules"
 | 
			
		||||
		],
 | 
			
		||||
		"lib": [
 | 
			
		||||
			"esnext"
 | 
			
		||||
		]
 | 
			
		||||
	},
 | 
			
		||||
	"compileOnSave": false,
 | 
			
		||||
	"include": [
 | 
			
		||||
		"./**/*.ts",
 | 
			
		||||
		"../src/**/*.ts"
 | 
			
		||||
	],
 | 
			
		||||
	"exclude": [
 | 
			
		||||
		"../src/**/*.test.ts"
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import * as crypto from 'node:crypto';
 | 
			
		|||
import cbor from 'cbor';
 | 
			
		||||
import * as OTPAuth from 'otpauth';
 | 
			
		||||
import { loadConfig } from '@/config.js';
 | 
			
		||||
import { api, signup, startServer } from '../utils.js';
 | 
			
		||||
import { api, signup } from '../utils.js';
 | 
			
		||||
import type {
 | 
			
		||||
	AuthenticationResponseJSON,
 | 
			
		||||
	AuthenticatorAssertionResponseJSON,
 | 
			
		||||
| 
						 | 
				
			
			@ -19,12 +19,10 @@ import type {
 | 
			
		|||
	PublicKeyCredentialRequestOptionsJSON,
 | 
			
		||||
	RegistrationResponseJSON,
 | 
			
		||||
} from '@simplewebauthn/typescript-types';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('2要素認証', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	const config = loadConfig();
 | 
			
		||||
	const password = 'test';
 | 
			
		||||
| 
						 | 
				
			
			@ -185,14 +183,9 @@ describe('2要素認証', () => {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username, password });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('が設定でき、OTPでログインできる。', async () => {
 | 
			
		||||
		const registerResponse = await api('/i/2fa/register', {
 | 
			
		||||
			password,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,24 +6,20 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { inspect } from 'node:util';
 | 
			
		||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import {
 | 
			
		||||
	signup,
 | 
			
		||||
	post,
 | 
			
		||||
	userList,
 | 
			
		||||
	page,
 | 
			
		||||
	role,
 | 
			
		||||
	startServer,
 | 
			
		||||
	api,
 | 
			
		||||
	successfulApiCall,
 | 
			
		||||
	failedApiCall,
 | 
			
		||||
	uploadFile,
 | 
			
		||||
	post,
 | 
			
		||||
	role,
 | 
			
		||||
	signup,
 | 
			
		||||
	successfulApiCall,
 | 
			
		||||
	testPaginationConsistency,
 | 
			
		||||
	uploadFile,
 | 
			
		||||
	userList,
 | 
			
		||||
} from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
 | 
			
		||||
	return selector(a).localeCompare(selector(b));
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +33,7 @@ describe('アンテナ', () => {
 | 
			
		|||
	// - srcのenumにgroupが残っている
 | 
			
		||||
	// - userGroupIdが残っている, isActiveがない
 | 
			
		||||
	type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
 | 
			
		||||
	type User = misskey.entities.MeSignup;
 | 
			
		||||
	type User = misskey.entities.SignupResponse;
 | 
			
		||||
	type Note = misskey.entities.Note;
 | 
			
		||||
 | 
			
		||||
	// アンテナを作成できる最小のパラメタ
 | 
			
		||||
| 
						 | 
				
			
			@ -54,8 +50,6 @@ describe('アンテナ', () => {
 | 
			
		|||
		withReplies: false,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let root: User;
 | 
			
		||||
	let alice: User;
 | 
			
		||||
	let bob: User;
 | 
			
		||||
| 
						 | 
				
			
			@ -79,10 +73,6 @@ describe('アンテナ', () => {
 | 
			
		|||
	let userMutingAlice: User;
 | 
			
		||||
	let userMutedByAlice: User;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		root = await signup({ username: 'root' });
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
| 
						 | 
				
			
			@ -136,10 +126,6 @@ describe('アンテナ', () => {
 | 
			
		|||
		await api('mute/create', { userId: userMutedByAlice.id }, alice);
 | 
			
		||||
	}, 1000 * 60 * 10);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	beforeEach(async () => {
 | 
			
		||||
		// テスト間で影響し合わないように毎回全部消す。
 | 
			
		||||
		for (const user of [alice, bob]) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,33 +6,22 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, signup } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('API visibility', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('Note visibility', () => {
 | 
			
		||||
		//#region vars
 | 
			
		||||
		/** ヒロイン */
 | 
			
		||||
		let alice: misskey.entities.MeSignup;
 | 
			
		||||
		let alice: misskey.entities.SignupResponse;
 | 
			
		||||
		/** フォロワー */
 | 
			
		||||
		let follower: misskey.entities.MeSignup;
 | 
			
		||||
		let follower: misskey.entities.SignupResponse;
 | 
			
		||||
		/** 非フォロワー */
 | 
			
		||||
		let other: misskey.entities.MeSignup;
 | 
			
		||||
		let other: misskey.entities.SignupResponse;
 | 
			
		||||
		/** 非フォロワーでもリプライやメンションをされた人 */
 | 
			
		||||
		let target: misskey.entities.MeSignup;
 | 
			
		||||
		let target: misskey.entities.SignupResponse;
 | 
			
		||||
		/** specified mentionでmentionを飛ばされる人 */
 | 
			
		||||
		let target2: misskey.entities.MeSignup;
 | 
			
		||||
		let target2: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
		/** public-post */
 | 
			
		||||
		let pub: any;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { IncomingMessage } from 'http';
 | 
			
		||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import {
 | 
			
		||||
	api,
 | 
			
		||||
	connectStream,
 | 
			
		||||
	createAppToken,
 | 
			
		||||
	failedApiCall,
 | 
			
		||||
	relativeFetch,
 | 
			
		||||
	signup,
 | 
			
		||||
	successfulApiCall,
 | 
			
		||||
	uploadFile,
 | 
			
		||||
	waitFire,
 | 
			
		||||
} from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('API', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('General validation', () => {
 | 
			
		||||
		test('wrong type', async () => {
 | 
			
		||||
			const res = await api('/test', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,29 +6,21 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, signup } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Block', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	// alice blocks bob
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('Block作成', async () => {
 | 
			
		||||
		const res = await api('/blocking/create', {
 | 
			
		||||
			userId: bob.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,25 +18,13 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf
 | 
			
		|||
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
 | 
			
		||||
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
 | 
			
		||||
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
 | 
			
		||||
import {
 | 
			
		||||
	signup,
 | 
			
		||||
	post,
 | 
			
		||||
	startServer,
 | 
			
		||||
	api,
 | 
			
		||||
	successfulApiCall,
 | 
			
		||||
	failedApiCall,
 | 
			
		||||
	ApiRequest,
 | 
			
		||||
	hiddenNote,
 | 
			
		||||
} from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
 | 
			
		||||
 | 
			
		||||
describe('クリップ', () => {
 | 
			
		||||
	type User = Packed<'User'>;
 | 
			
		||||
	type Note = Packed<'Note'>;
 | 
			
		||||
	type Clip = Packed<'Clip'>;
 | 
			
		||||
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: User;
 | 
			
		||||
	let bob: User;
 | 
			
		||||
	let aliceNote: Note;
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +133,6 @@ describe('クリップ', () => {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,10 +147,6 @@ describe('クリップ', () => {
 | 
			
		|||
		bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	afterEach(async () => {
 | 
			
		||||
		// テスト間で影響し合わないように毎回全部消す。
 | 
			
		||||
		for (const user of [alice, bob]) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,30 +10,22 @@ import * as assert from 'assert';
 | 
			
		|||
// https://github.com/node-fetch/node-fetch/pull/1664
 | 
			
		||||
import { Blob } from 'node-fetch';
 | 
			
		||||
import { MiUser } from '@/models/_.js';
 | 
			
		||||
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Endpoints', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let dave: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
	let dave: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
		dave = await signup({ username: 'dave' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('signup', () => {
 | 
			
		||||
		test('不正なユーザー名でアカウントが作成できない', async () => {
 | 
			
		||||
			const res = await api('signup', {
 | 
			
		||||
| 
						 | 
				
			
			@ -710,6 +702,18 @@ describe('Endpoints', () => {
 | 
			
		|||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('不正なファイル名で怒られる', async () => {
 | 
			
		||||
			const file = (await uploadFile(alice)).body;
 | 
			
		||||
			const newName = '';
 | 
			
		||||
 | 
			
		||||
			const res = await api('/drive/files/update', {
 | 
			
		||||
				fileId: file.id,
 | 
			
		||||
				name: newName,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test('間違ったIDで怒られる', async () => {
 | 
			
		||||
			const res = await api('/drive/files/update', {
 | 
			
		||||
				fileId: 'kyoppie',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										193
									
								
								packages/backend/test/e2e/exports.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								packages/backend/test/e2e/exports.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,193 @@
 | 
			
		|||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { api, port, post, signup, startJobQueue } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('export-clips', () => {
 | 
			
		||||
	let queue: INestApplicationContext;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	// XXX: Any better way to get the result?
 | 
			
		||||
	async function pollFirstDriveFile() {
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const files = (await api('/drive/files', {}, alice)).body;
 | 
			
		||||
			if (!files.length) {
 | 
			
		||||
				await new Promise(r => setTimeout(r, 100));
 | 
			
		||||
				continue;
 | 
			
		||||
			}
 | 
			
		||||
			if (files.length > 1) {
 | 
			
		||||
				throw new Error('Too many files?');
 | 
			
		||||
			}
 | 
			
		||||
			const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
 | 
			
		||||
			const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
 | 
			
		||||
			return await res.json();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		queue = await startJobQueue();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await queue.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	beforeEach(async () => {
 | 
			
		||||
		// Clean all clips and files of alice
 | 
			
		||||
		const clips = (await api('/clips/list', {}, alice)).body;
 | 
			
		||||
		for (const clip of clips) {
 | 
			
		||||
			const res = await api('/clips/delete', { clipId: clip.id }, alice);
 | 
			
		||||
			if (res.status !== 204) {
 | 
			
		||||
				throw new Error('Failed to delete clip');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const files = (await api('/drive/files', {}, alice)).body;
 | 
			
		||||
		for (const file of files) {
 | 
			
		||||
			const res = await api('/drive/files/delete', { fileId: file.id }, alice);
 | 
			
		||||
			if (res.status !== 204) {
 | 
			
		||||
				throw new Error('Failed to delete file');
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('basic export', async () => {
 | 
			
		||||
		let res = await api('/clips/create', {
 | 
			
		||||
			name: 'foo',
 | 
			
		||||
			description: 'bar',
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
 | 
			
		||||
		res = await api('/i/export-clips', {}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		const exported = await pollFirstDriveFile();
 | 
			
		||||
		assert.strictEqual(exported[0].name, 'foo');
 | 
			
		||||
		assert.strictEqual(exported[0].description, 'bar');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes.length, 0);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('export with notes', async () => {
 | 
			
		||||
		let res = await api('/clips/create', {
 | 
			
		||||
			name: 'foo',
 | 
			
		||||
			description: 'bar',
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
		const clip = res.body;
 | 
			
		||||
 | 
			
		||||
		const note1 = await post(alice, {
 | 
			
		||||
			text: 'baz1',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const note2 = await post(alice, {
 | 
			
		||||
			text: 'baz2',
 | 
			
		||||
			poll: {
 | 
			
		||||
				choices: ['sakura', 'izumi', 'ako'],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		for (const note of [note1, note2]) {
 | 
			
		||||
			res = await api('/clips/add-note', {
 | 
			
		||||
				clipId: clip.id,
 | 
			
		||||
				noteId: note.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(res.status, 204);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res = await api('/i/export-clips', {}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		const exported = await pollFirstDriveFile();
 | 
			
		||||
		assert.strictEqual(exported[0].name, 'foo');
 | 
			
		||||
		assert.strictEqual(exported[0].description, 'bar');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes.length, 2);
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2');
 | 
			
		||||
		assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('multiple clips', async () => {
 | 
			
		||||
		let res = await api('/clips/create', {
 | 
			
		||||
			name: 'kawaii',
 | 
			
		||||
			description: 'kawaii',
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
		const clip1 = res.body;
 | 
			
		||||
 | 
			
		||||
		res = await api('/clips/create', {
 | 
			
		||||
			name: 'yuri',
 | 
			
		||||
			description: 'yuri',
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
		const clip2 = res.body;
 | 
			
		||||
 | 
			
		||||
		const note1 = await post(alice, {
 | 
			
		||||
			text: 'baz1',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const note2 = await post(alice, {
 | 
			
		||||
			text: 'baz2',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		res = await api('/clips/add-note', {
 | 
			
		||||
			clipId: clip1.id,
 | 
			
		||||
			noteId: note1.id,
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		res = await api('/clips/add-note', {
 | 
			
		||||
			clipId: clip2.id,
 | 
			
		||||
			noteId: note2.id,
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		res = await api('/i/export-clips', {}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		const exported = await pollFirstDriveFile();
 | 
			
		||||
		assert.strictEqual(exported[0].name, 'kawaii');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes.length, 1);
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
 | 
			
		||||
		assert.strictEqual(exported[1].name, 'yuri');
 | 
			
		||||
		assert.strictEqual(exported[1].clipNotes.length, 1);
 | 
			
		||||
		assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('Clipping other user\'s note', async () => {
 | 
			
		||||
		let res = await api('/clips/create', {
 | 
			
		||||
			name: 'kawaii',
 | 
			
		||||
			description: 'kawaii',
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
		const clip = res.body;
 | 
			
		||||
 | 
			
		||||
		const note = await post(bob, {
 | 
			
		||||
			text: 'baz',
 | 
			
		||||
			visibility: 'followers',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		res = await api('/clips/add-note', {
 | 
			
		||||
			clipId: clip.id,
 | 
			
		||||
			noteId: note.id,
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		res = await api('/i/export-clips', {}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
 | 
			
		||||
		const exported = await pollFirstDriveFile();
 | 
			
		||||
		assert.strictEqual(exported[0].name, 'kawaii');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes.length, 1);
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz');
 | 
			
		||||
		assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob');
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -6,9 +6,8 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js';
 | 
			
		||||
import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
 | 
			
		||||
import type { SimpleGetResponse } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
// Request Accept
 | 
			
		||||
| 
						 | 
				
			
			@ -23,9 +22,7 @@ const HTML = 'text/html; charset=utf-8';
 | 
			
		|||
const JSON_UTF8 = 'application/json; charset=utf-8';
 | 
			
		||||
 | 
			
		||||
describe('Webリソース', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let aliceUploadedFile: any;
 | 
			
		||||
	let alicesPost: any;
 | 
			
		||||
	let alicePage: any;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +31,7 @@ describe('Webリソース', () => {
 | 
			
		|||
	let aliceGalleryPost: any;
 | 
			
		||||
	let aliceChannel: any;
 | 
			
		||||
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	type Request = {
 | 
			
		||||
		path: string,
 | 
			
		||||
| 
						 | 
				
			
			@ -79,7 +76,6 @@ describe('Webリソース', () => {
 | 
			
		|||
	};
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		aliceUploadedFile = await uploadFile(alice);
 | 
			
		||||
		alicesPost = await post(alice, {
 | 
			
		||||
| 
						 | 
				
			
			@ -96,10 +92,6 @@ describe('Webリソース', () => {
 | 
			
		|||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe.each([
 | 
			
		||||
		{ path: '/', type: HTML },
 | 
			
		||||
		{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,26 +6,18 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, startServer, simpleGet } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, signup, simpleGet } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('FF visibility', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
 | 
			
		||||
		await api('/i/update', {
 | 
			
		||||
			followingVisibility: 'public',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,35 +3,35 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { loadConfig } from '@/config.js';
 | 
			
		||||
import { MiUser, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { jobQueue } from '@/boot/common.js';
 | 
			
		||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
 | 
			
		||||
import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { jobQueue } from '@/boot/common.js';
 | 
			
		||||
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Account Move', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let jq: INestApplicationContext;
 | 
			
		||||
	let url: URL;
 | 
			
		||||
 | 
			
		||||
	let root: any;
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let dave: misskey.entities.MeSignup;
 | 
			
		||||
	let eve: misskey.entities.MeSignup;
 | 
			
		||||
	let frank: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
	let dave: misskey.entities.SignupResponse;
 | 
			
		||||
	let eve: misskey.entities.SignupResponse;
 | 
			
		||||
	let frank: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	let Users: UsersRepository;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		jq = await jobQueue();
 | 
			
		||||
 | 
			
		||||
		const config = loadConfig();
 | 
			
		||||
		url = new URL(config.url);
 | 
			
		||||
		const connection = await initTestDb(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ describe('Account Move', () => {
 | 
			
		|||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await Promise.all([app.close(), jq.close()]);
 | 
			
		||||
		await jq.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('Create Alias', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,29 +6,21 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, react, signup, waitFire } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Mute', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	// alice mutes carol
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ミュート作成', async () => {
 | 
			
		||||
		const res = await api('/mute/create', {
 | 
			
		||||
			userId: carol.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,20 +6,9 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { relativeFetch, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { relativeFetch } from '../utils.js';
 | 
			
		||||
 | 
			
		||||
describe('nodeinfo', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('nodeinfo 2.1', async () => {
 | 
			
		||||
		const res = await relativeFetch('nodeinfo/2.1');
 | 
			
		||||
		assert.ok(res.ok);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,29 +8,22 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
import * as assert from 'assert';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
 | 
			
		||||
import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Note', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let Notes: any;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		const connection = await initTestDb(true);
 | 
			
		||||
		Notes = connection.getRepository(MiNote);
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('投稿できる', async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,18 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2';
 | 
			
		||||
import {
 | 
			
		||||
	AuthorizationCode,
 | 
			
		||||
	type AuthorizationTokenConfig,
 | 
			
		||||
	ClientCredentials,
 | 
			
		||||
	ModuleOptions,
 | 
			
		||||
	ResourceOwnerPassword,
 | 
			
		||||
} from 'simple-oauth2';
 | 
			
		||||
import pkceChallenge from 'pkce-challenge';
 | 
			
		||||
import { JSDOM } from 'jsdom';
 | 
			
		||||
import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify';
 | 
			
		||||
import { api, port, signup, startServer } from '../utils.js';
 | 
			
		||||
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
 | 
			
		||||
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
const host = `http://127.0.0.1:${port}`;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +80,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName:
 | 
			
		|||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
 | 
			
		||||
function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
 | 
			
		||||
	return fetch(new URL('/oauth/decision', host), {
 | 
			
		||||
		method: 'post',
 | 
			
		||||
		body: new URLSearchParams({
 | 
			
		||||
| 
						 | 
				
			
			@ -90,14 +95,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, {
 | 
			
		|||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
 | 
			
		||||
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
 | 
			
		||||
	const { transactionId } = getMeta(await response.text());
 | 
			
		||||
	assert.ok(transactionId);
 | 
			
		||||
 | 
			
		||||
	return await fetchDecision(transactionId, user, { cancel });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
 | 
			
		||||
async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
 | 
			
		||||
	const client = new AuthorizationCode(clientConfig);
 | 
			
		||||
 | 
			
		||||
	const response = await fetch(client.authorizeURL({
 | 
			
		||||
| 
						 | 
				
			
			@ -147,16 +152,14 @@ async function assertDirectError(response: Response, status: number, error: stri
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
describe('OAuth', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let fastify: FastifyInstance;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	let sender: (reply: FastifyReply) => void;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -168,7 +171,7 @@ describe('OAuth', () => {
 | 
			
		|||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	beforeEach(async () => {
 | 
			
		||||
		process.env.MISSKEY_TEST_CHECK_IP_RANGE = '';
 | 
			
		||||
		await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' });
 | 
			
		||||
		sender = (reply): void => {
 | 
			
		||||
			reply.send(`
 | 
			
		||||
				<!DOCTYPE html>
 | 
			
		||||
| 
						 | 
				
			
			@ -180,7 +183,6 @@ describe('OAuth', () => {
 | 
			
		|||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await fastify.close();
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('Full flow', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -881,7 +883,7 @@ describe('OAuth', () => {
 | 
			
		|||
		});
 | 
			
		||||
 | 
			
		||||
		test('Disallow loopback', async () => {
 | 
			
		||||
			process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1';
 | 
			
		||||
			await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' });
 | 
			
		||||
 | 
			
		||||
			const client = new AuthorizationCode(clientConfig);
 | 
			
		||||
			const response = await fetch(client.authorizeURL({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,29 +6,21 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, signup, sleep, waitFire } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Renote Mute', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	// alice mutes carol
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('ミュート作成', async () => {
 | 
			
		||||
		const res = await api('/renote-mute/create', {
 | 
			
		||||
			userId: carol.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,12 +8,10 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
import * as assert from 'assert';
 | 
			
		||||
import { WebSocket } from 'ws';
 | 
			
		||||
import { MiFollowing } from '@/models/Following.js';
 | 
			
		||||
import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Streaming', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let Followings: any;
 | 
			
		||||
 | 
			
		||||
	const follow = async (follower: any, followee: any) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,15 +30,15 @@ describe('Streaming', () => {
 | 
			
		|||
 | 
			
		||||
	describe('Streaming', () => {
 | 
			
		||||
		// Local users
 | 
			
		||||
		let ayano: misskey.entities.MeSignup;
 | 
			
		||||
		let kyoko: misskey.entities.MeSignup;
 | 
			
		||||
		let chitose: misskey.entities.MeSignup;
 | 
			
		||||
		let kanako: misskey.entities.MeSignup;
 | 
			
		||||
		let ayano: misskey.entities.SignupResponse;
 | 
			
		||||
		let kyoko: misskey.entities.SignupResponse;
 | 
			
		||||
		let chitose: misskey.entities.SignupResponse;
 | 
			
		||||
		let kanako: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
		// Remote users
 | 
			
		||||
		let akari: misskey.entities.MeSignup;
 | 
			
		||||
		let chinatsu: misskey.entities.MeSignup;
 | 
			
		||||
		let takumi: misskey.entities.MeSignup;
 | 
			
		||||
		let akari: misskey.entities.SignupResponse;
 | 
			
		||||
		let chinatsu: misskey.entities.SignupResponse;
 | 
			
		||||
		let takumi: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
		let kyokoNote: any;
 | 
			
		||||
		let kanakoNote: any;
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +46,6 @@ describe('Streaming', () => {
 | 
			
		|||
		let list: any;
 | 
			
		||||
 | 
			
		||||
		beforeAll(async () => {
 | 
			
		||||
			app = await startServer();
 | 
			
		||||
			const connection = await initTestDb(true);
 | 
			
		||||
			Followings = connection.getRepository(MiFollowing);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -95,10 +92,6 @@ describe('Streaming', () => {
 | 
			
		|||
			}, chitose);
 | 
			
		||||
		}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
		afterAll(async () => {
 | 
			
		||||
			await app.close();
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		describe('Events', () => {
 | 
			
		||||
			test('mention event', async () => {
 | 
			
		||||
				const fired = await waitFire(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,28 +6,20 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, connectStream, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, connectStream, post, signup } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('Note thread mute', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let bob: misskey.entities.MeSignup;
 | 
			
		||||
	let carol: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let bob: misskey.entities.SignupResponse;
 | 
			
		||||
	let carol: misskey.entities.SignupResponse;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		bob = await signup({ username: 'bob' });
 | 
			
		||||
		carol = await signup({ username: 'carol' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
 | 
			
		||||
		const bobNote = await post(bob, { text: '@alice @carol root note' });
 | 
			
		||||
		const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,12 +6,8 @@
 | 
			
		|||
// How to run:
 | 
			
		||||
// pnpm jest -- e2e/timelines.ts
 | 
			
		||||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js';
 | 
			
		||||
 | 
			
		||||
function genHost() {
 | 
			
		||||
	return randomString() + '.example.com';
 | 
			
		||||
| 
						 | 
				
			
			@ -21,16 +17,6 @@ function waitForPushToTl() {
 | 
			
		|||
	return sleep(500);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
beforeAll(async () => {
 | 
			
		||||
	app = await startServer();
 | 
			
		||||
}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
afterAll(async () => {
 | 
			
		||||
	await app.close();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('Timelines', () => {
 | 
			
		||||
	describe('Home TL', () => {
 | 
			
		||||
		test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -334,8 +320,9 @@ describe('Timelines', () => {
 | 
			
		|||
		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 | 
			
		||||
 | 
			
		||||
			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
| 
						 | 
				
			
			@ -348,8 +335,9 @@ describe('Timelines', () => {
 | 
			
		|||
		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 | 
			
		||||
 | 
			
		||||
			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
| 
						 | 
				
			
			@ -762,8 +750,9 @@ describe('Timelines', () => {
 | 
			
		|||
		test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 | 
			
		||||
 | 
			
		||||
			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
| 
						 | 
				
			
			@ -776,8 +765,9 @@ describe('Timelines', () => {
 | 
			
		|||
		test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
 | 
			
		||||
 | 
			
		||||
			await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,20 +6,16 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { signup, api, post, uploadUrl, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { api, post, signup, uploadUrl } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('users/notes', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let alice: misskey.entities.MeSignup;
 | 
			
		||||
	let alice: misskey.entities.SignupResponse;
 | 
			
		||||
	let jpgNote: any;
 | 
			
		||||
	let pngNote: any;
 | 
			
		||||
	let jpgPngNote: any;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
 | 
			
		||||
		const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
 | 
			
		||||
| 
						 | 
				
			
			@ -34,10 +30,6 @@ describe('users/notes', () => {
 | 
			
		|||
		});
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async() => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('withFiles', async () => {
 | 
			
		||||
		const res = await api('/users/notes', {
 | 
			
		||||
			userId: alice.id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,20 +8,8 @@ process.env.NODE_ENV = 'test';
 | 
			
		|||
import * as assert from 'assert';
 | 
			
		||||
import { inspect } from 'node:util';
 | 
			
		||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
 | 
			
		||||
import type { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import {
 | 
			
		||||
	signup,
 | 
			
		||||
	post,
 | 
			
		||||
	page,
 | 
			
		||||
	role,
 | 
			
		||||
	startServer,
 | 
			
		||||
	api,
 | 
			
		||||
	successfulApiCall,
 | 
			
		||||
	failedApiCall,
 | 
			
		||||
	uploadFile,
 | 
			
		||||
} from '../utils.js';
 | 
			
		||||
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
describe('ユーザー', () => {
 | 
			
		||||
	// エンティティとしてのユーザーを主眼においたテストを記述する
 | 
			
		||||
| 
						 | 
				
			
			@ -188,8 +176,6 @@ describe('ユーザー', () => {
 | 
			
		|||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
 | 
			
		||||
	let root: User;
 | 
			
		||||
	let alice: User;
 | 
			
		||||
	let aliceNote: misskey.entities.Note;
 | 
			
		||||
| 
						 | 
				
			
			@ -233,10 +219,6 @@ describe('ユーザー', () => {
 | 
			
		|||
	let userFollowRequesting: User;
 | 
			
		||||
	let userFollowRequested: User;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		root = await signup({ username: 'root' });
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
| 
						 | 
				
			
			@ -324,10 +306,6 @@ describe('ユーザー', () => {
 | 
			
		|||
		await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting);
 | 
			
		||||
	}, 1000 * 60 * 10);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	beforeEach(async () => {
 | 
			
		||||
		alice = {
 | 
			
		||||
			...alice,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,24 +6,16 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { host, origin, relativeFetch, signup, startServer } from '../utils.js';
 | 
			
		||||
import type { INestApplicationContext } from '@nestjs/common';
 | 
			
		||||
import { host, origin, relativeFetch, signup } from '../utils.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
describe('.well-known', () => {
 | 
			
		||||
	let app: INestApplicationContext;
 | 
			
		||||
	let alice: misskey.entities.User;
 | 
			
		||||
 | 
			
		||||
	beforeAll(async () => {
 | 
			
		||||
		app = await startServer();
 | 
			
		||||
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
	}, 1000 * 60 * 2);
 | 
			
		||||
 | 
			
		||||
	afterAll(async () => {
 | 
			
		||||
		await app.close();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('nodeinfo', async () => {
 | 
			
		||||
		const res = await relativeFetch('.well-known/nodeinfo');
 | 
			
		||||
		assert.ok(res.ok);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								packages/backend/test/jest.setup.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								packages/backend/test/jest.setup.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { initTestDb, sendEnvResetRequest } from './utils.js';
 | 
			
		||||
 | 
			
		||||
beforeAll(async () => {
 | 
			
		||||
	await Promise.all([
 | 
			
		||||
		initTestDb(false),
 | 
			
		||||
		sendEnvResetRequest(),
 | 
			
		||||
	]);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +15,13 @@ import type { LoggerService } from '@/core/LoggerService.js';
 | 
			
		|||
import type { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import type { UtilityService } from '@/core/UtilityService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
 | 
			
		||||
import type {
 | 
			
		||||
	FollowRequestsRepository,
 | 
			
		||||
	NoteReactionsRepository,
 | 
			
		||||
	NotesRepository,
 | 
			
		||||
	PollsRepository,
 | 
			
		||||
	UsersRepository,
 | 
			
		||||
} from '@/models/_.js';
 | 
			
		||||
 | 
			
		||||
type MockResponse = {
 | 
			
		||||
	type: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock';
 | 
			
		|||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
 | 
			
		||||
import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js';
 | 
			
		||||
import type {
 | 
			
		||||
	AnnouncementReadsRepository,
 | 
			
		||||
	AnnouncementsRepository,
 | 
			
		||||
	MiAnnouncement,
 | 
			
		||||
	MiUser,
 | 
			
		||||
	UsersRepository,
 | 
			
		||||
} from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { genAidx } from '@/misc/id/aidx.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,13 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3';
 | 
			
		||||
import {
 | 
			
		||||
	DeleteObjectCommand,
 | 
			
		||||
	DeleteObjectCommandOutput,
 | 
			
		||||
	InvalidObjectState,
 | 
			
		||||
	NoSuchKey,
 | 
			
		||||
	S3Client,
 | 
			
		||||
} from '@aws-sdk/client-s3';
 | 
			
		||||
import { mockClient } from 'aws-sdk-client-mock';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { DriveService } from '@/core/DriveService.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,8 @@ describe('FetchInstanceMetadataService', () => {
 | 
			
		|||
					return { fetch: jest.fn() };
 | 
			
		||||
				} else if (token === DI.redis) {
 | 
			
		||||
					return mockRedis;
 | 
			
		||||
				}})
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.compile();
 | 
			
		||||
 | 
			
		||||
		app.enableShutdownHooks();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
 | 
			
		|||
import { dirname } from 'node:path';
 | 
			
		||||
import { ModuleMocker } from 'jest-mock';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { describe, beforeAll, afterAll, test } from '@jest/globals';
 | 
			
		||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { FileInfoService } from '@/core/FileInfoService.js';
 | 
			
		||||
//import { DI } from '@/di-symbols.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,15 +6,13 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import { jest } from '@jest/globals';
 | 
			
		||||
import { ModuleMocker } from 'jest-mock';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import type { MetasRepository } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { CoreModule } from '@/core/CoreModule.js';
 | 
			
		||||
import type { DataSource } from 'typeorm';
 | 
			
		||||
import type { TestingModule } from '@nestjs/testing';
 | 
			
		||||
import type { DataSource } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
describe('MetaService', () => {
 | 
			
		||||
	let app: TestingModule;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import { Test } from '@nestjs/testing';
 | 
			
		|||
import * as lolex from '@sinonjs/fake-timers';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { RoleService } from '@/core/RoleService.js';
 | 
			
		||||
import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js';
 | 
			
		||||
import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { MetaService } from '@/core/MetaService.js';
 | 
			
		||||
import { genAidx } from '@/misc/id/aidx.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,13 @@
 | 
			
		|||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
 | 
			
		||||
import {
 | 
			
		||||
	CompleteMultipartUploadCommand,
 | 
			
		||||
	CreateMultipartUploadCommand,
 | 
			
		||||
	PutObjectCommand,
 | 
			
		||||
	S3Client,
 | 
			
		||||
	UploadPartCommand,
 | 
			
		||||
} from '@aws-sdk/client-s3';
 | 
			
		||||
import { mockClient } from 'aws-sdk-client-mock';
 | 
			
		||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
			
		||||
import { CoreModule } from '@/core/CoreModule.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,13 +4,13 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import { ulid } from 'ulid';
 | 
			
		||||
import { describe, test, expect } from '@jest/globals';
 | 
			
		||||
import { describe, expect, test } from '@jest/globals';
 | 
			
		||||
import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js';
 | 
			
		||||
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
 | 
			
		||||
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
 | 
			
		||||
import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js';
 | 
			
		||||
import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js';
 | 
			
		||||
import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js';
 | 
			
		||||
import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js';
 | 
			
		||||
 | 
			
		||||
describe('misc:id', () => {
 | 
			
		||||
	test('aid', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { describe, test, expect } from '@jest/globals';
 | 
			
		||||
import { describe, expect, test } from '@jest/globals';
 | 
			
		||||
import { contentDisposition } from '@/misc/content-disposition.js';
 | 
			
		||||
 | 
			
		||||
describe('misc:content-disposition', () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,7 @@
 | 
			
		|||
 | 
			
		||||
import * as assert from 'node:assert';
 | 
			
		||||
import { readFile } from 'node:fs/promises';
 | 
			
		||||
import { isAbsolute, basename } from 'node:path';
 | 
			
		||||
import { basename, isAbsolute } from 'node:path';
 | 
			
		||||
import { randomUUID } from 'node:crypto';
 | 
			
		||||
import { inspect } from 'node:util';
 | 
			
		||||
import WebSocket, { ClientOptions } from 'ws';
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js';
 | 
			
		|||
import { loadConfig } from '../src/config.js';
 | 
			
		||||
import type * as misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
export { server as startServer } from '@/boot/common.js';
 | 
			
		||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
 | 
			
		||||
 | 
			
		||||
interface UserToken {
 | 
			
		||||
	token: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +68,11 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
 | 
			
		|||
	return res.body;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => {
 | 
			
		||||
const request = async (path: string, params: any, me?: UserToken): Promise<{
 | 
			
		||||
	status: number,
 | 
			
		||||
	headers: Headers,
 | 
			
		||||
	body: any
 | 
			
		||||
}> => {
 | 
			
		||||
	const bodyAuth: Record<string, string> = {};
 | 
			
		||||
	const headers: Record<string, string> = {
 | 
			
		||||
		'Content-Type': 'application/json',
 | 
			
		||||
| 
						 | 
				
			
			@ -275,7 +279,11 @@ interface UploadOptions {
 | 
			
		|||
 * Upload file
 | 
			
		||||
 * @param user User
 | 
			
		||||
 */
 | 
			
		||||
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => {
 | 
			
		||||
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
 | 
			
		||||
	status: number,
 | 
			
		||||
	headers: Headers,
 | 
			
		||||
	body: misskey.Endpoints['drive/files/create']['res'] | null
 | 
			
		||||
}> => {
 | 
			
		||||
	const absPath = path == null
 | 
			
		||||
		? new URL('resources/Lenna.jpg', import.meta.url)
 | 
			
		||||
		: isAbsolute(path.toString())
 | 
			
		||||
| 
						 | 
				
			
			@ -557,3 +565,34 @@ export function sleep(msec: number) {
 | 
			
		|||
		}, msec);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
 | 
			
		||||
	const res = await fetch(
 | 
			
		||||
		`http://localhost:${port + 1000}/env`,
 | 
			
		||||
		{
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
			body: JSON.stringify(params),
 | 
			
		||||
		},
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (res.status !== 200) {
 | 
			
		||||
		throw new Error('server env update failed.');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function sendEnvResetRequest() {
 | 
			
		||||
	const res = await fetch(
 | 
			
		||||
		`http://localhost:${port + 1000}/env-reset`,
 | 
			
		||||
		{
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({}),
 | 
			
		||||
		},
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	if (res.status !== 200) {
 | 
			
		||||
		throw new Error('server env update failed.');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/bgm_1.mp3
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/bgm_1.mp3
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/bubble2.mp3
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/bubble2.mp3
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/cold_face.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/cold_face.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										6
									
								
								packages/frontend/assets/drop-and-fusion/drop-arrow.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								packages/frontend/assets/drop-and-fusion/drop-arrow.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
 | 
			
		||||
    <path d="M0,0L128,0L64,64L0,0Z" style="fill:rgb(255,61,0);"/>
 | 
			
		||||
    <path d="M0,0L128,0L64,64L0,0ZM28.971,12L64,47.029C64,47.029 99.029,12 99.029,12L28.971,12Z" style="fill:rgb(255,122,0);"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 646 B  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/dropper.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/frontend/assets/drop-and-fusion/dropper.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 32 KiB  | 
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue