mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-22 22:13:09 +02:00
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_PASSWORD=example-misskey-pass
|
||||||
POSTGRES_USER=example-misskey-user
|
POSTGRES_USER=example-misskey-user
|
||||||
POSTGRES_DB=misskey
|
POSTGRES_DB=misskey
|
||||||
|
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"
|
||||||
|
|
|
@ -8,6 +8,12 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- packages/**
|
- packages/**
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/frontend/**
|
||||||
|
- packages/sw/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- packages/shared/.eslintrc.js
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pnpm_install:
|
pnpm_install:
|
||||||
|
|
|
@ -19,4 +19,4 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
label: Do you want to implement this feature yourself?
|
label: Do you want to implement this feature yourself?
|
||||||
options:
|
options:
|
||||||
- label: Yes, I will implement this by myself and send a pull request
|
- label: Yes, I will implement this by myself and send a pull request
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -41,6 +41,7 @@ docker-compose.yml
|
||||||
# misskey
|
# misskey
|
||||||
/build
|
/build
|
||||||
built
|
built
|
||||||
|
built-test
|
||||||
/data
|
/data
|
||||||
/.cache-loader
|
/.cache-loader
|
||||||
/db
|
/db
|
||||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,16 +1,22 @@
|
||||||
<!--
|
## 202x.x.x (Unreleased)
|
||||||
## 2023.x.x (unreleased)
|
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
|
||||||
|
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
|
||||||
|
|
||||||
### Client
|
### 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
|
### Server
|
||||||
-
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||||
-->
|
- Enhance: クリップをエクスポートできるように
|
||||||
|
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
|
||||||
|
|
||||||
## 2023.12.2
|
## 2023.12.2
|
||||||
|
|
||||||
|
|
2
COPYING
2
COPYING
|
@ -1,5 +1,5 @@
|
||||||
Unless otherwise stated this repository is
|
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.
|
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.
|
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 (backend)~~ → Done ✔️
|
||||||
|
- Make the number of type errors zero (frontend)
|
||||||
- Improve CI
|
- Improve CI
|
||||||
- ~~Fix tests~~ → Done ✔️
|
- ~~Fix tests~~ → Done ✔️
|
||||||
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
|
- 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:
|
links:
|
||||||
- db
|
- db
|
||||||
- redis
|
- redis
|
||||||
|
# - mcaptcha
|
||||||
# - meilisearch
|
# - meilisearch
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
|
@ -48,6 +49,36 @@ services:
|
||||||
interval: 5s
|
interval: 5s
|
||||||
retries: 20
|
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:
|
# meilisearch:
|
||||||
# restart: always
|
# restart: always
|
||||||
# image: getmeili/meilisearch:v1.3.4
|
# image: getmeili/meilisearch:v1.3.4
|
||||||
|
|
|
@ -127,7 +127,7 @@ reaction: "Reactions"
|
||||||
reactions: "Reactions"
|
reactions: "Reactions"
|
||||||
emojiPicker: "Emoji picker"
|
emojiPicker: "Emoji picker"
|
||||||
pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
|
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"
|
emojiPickerDisplay: "Emoji picker display"
|
||||||
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
|
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
|
||||||
overwriteFromPinnedEmojis: "Override from general 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;
|
"enableHcaptcha": string;
|
||||||
"hcaptchaSiteKey": string;
|
"hcaptchaSiteKey": string;
|
||||||
"hcaptchaSecretKey": string;
|
"hcaptchaSecretKey": string;
|
||||||
|
"mcaptcha": string;
|
||||||
|
"enableMcaptcha": string;
|
||||||
|
"mcaptchaSiteKey": string;
|
||||||
|
"mcaptchaSecretKey": string;
|
||||||
|
"mcaptchaInstanceUrl": string;
|
||||||
"recaptcha": string;
|
"recaptcha": string;
|
||||||
"enableRecaptcha": string;
|
"enableRecaptcha": string;
|
||||||
"recaptchaSiteKey": string;
|
"recaptchaSiteKey": string;
|
||||||
|
@ -686,6 +691,7 @@ export interface Locale {
|
||||||
"other": string;
|
"other": string;
|
||||||
"regenerateLoginToken": string;
|
"regenerateLoginToken": string;
|
||||||
"regenerateLoginTokenDescription": string;
|
"regenerateLoginTokenDescription": string;
|
||||||
|
"theKeywordWhenSearchingForCustomEmoji": string;
|
||||||
"setMultipleBySeparatingWithSpace": string;
|
"setMultipleBySeparatingWithSpace": string;
|
||||||
"fileIdOrUrl": string;
|
"fileIdOrUrl": string;
|
||||||
"behavior": string;
|
"behavior": string;
|
||||||
|
@ -1226,6 +1232,7 @@ export interface Locale {
|
||||||
"decorate": string;
|
"decorate": string;
|
||||||
"addMfmFunction": string;
|
"addMfmFunction": string;
|
||||||
"enableQuickAddMfmFunction": string;
|
"enableQuickAddMfmFunction": string;
|
||||||
|
"bubbleGame": string;
|
||||||
"_announcement": {
|
"_announcement": {
|
||||||
"forExistingUsers": string;
|
"forExistingUsers": string;
|
||||||
"forExistingUsersDescription": string;
|
"forExistingUsersDescription": string;
|
||||||
|
@ -1690,6 +1697,15 @@ export interface Locale {
|
||||||
"title": string;
|
"title": string;
|
||||||
"description": string;
|
"description": string;
|
||||||
};
|
};
|
||||||
|
"_bubbleGameExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
};
|
||||||
|
"_bubbleGameDoubleExplodingHead": {
|
||||||
|
"title": string;
|
||||||
|
"description": string;
|
||||||
|
"flavor": string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_role": {
|
"_role": {
|
||||||
|
@ -2302,6 +2318,7 @@ export interface Locale {
|
||||||
"_exportOrImport": {
|
"_exportOrImport": {
|
||||||
"allNotes": string;
|
"allNotes": string;
|
||||||
"favoritedNotes": string;
|
"favoritedNotes": string;
|
||||||
|
"clips": string;
|
||||||
"followingList": string;
|
"followingList": string;
|
||||||
"muteList": string;
|
"muteList": string;
|
||||||
"blockingList": string;
|
"blockingList": string;
|
||||||
|
|
|
@ -390,6 +390,11 @@ hcaptcha: "hCaptcha"
|
||||||
enableHcaptcha: "hCaptchaを有効にする"
|
enableHcaptcha: "hCaptchaを有効にする"
|
||||||
hcaptchaSiteKey: "サイトキー"
|
hcaptchaSiteKey: "サイトキー"
|
||||||
hcaptchaSecretKey: "シークレットキー"
|
hcaptchaSecretKey: "シークレットキー"
|
||||||
|
mcaptcha: "mCaptcha"
|
||||||
|
enableMcaptcha: "mCaptchaを有効にする"
|
||||||
|
mcaptchaSiteKey: "サイトキー"
|
||||||
|
mcaptchaSecretKey: "シークレットキー"
|
||||||
|
mcaptchaInstanceUrl: "mCaptchaのインスタンスのURL"
|
||||||
recaptcha: "reCAPTCHA"
|
recaptcha: "reCAPTCHA"
|
||||||
enableRecaptcha: "reCAPTCHAを有効にする"
|
enableRecaptcha: "reCAPTCHAを有効にする"
|
||||||
recaptchaSiteKey: "サイトキー"
|
recaptchaSiteKey: "サイトキー"
|
||||||
|
@ -683,6 +688,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
|
||||||
other: "その他"
|
other: "その他"
|
||||||
regenerateLoginToken: "ログイントークンを再生成"
|
regenerateLoginToken: "ログイントークンを再生成"
|
||||||
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
|
||||||
|
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
|
||||||
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
|
||||||
fileIdOrUrl: "ファイルIDまたはURL"
|
fileIdOrUrl: "ファイルIDまたはURL"
|
||||||
behavior: "動作"
|
behavior: "動作"
|
||||||
|
@ -1223,6 +1229,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
||||||
decorate: "デコる"
|
decorate: "デコる"
|
||||||
addMfmFunction: "装飾を追加"
|
addMfmFunction: "装飾を追加"
|
||||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||||
|
bubbleGame: "バブルゲーム"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
@ -1601,6 +1608,13 @@ _achievements:
|
||||||
_tutorialCompleted:
|
_tutorialCompleted:
|
||||||
title: "Sharkey初心者講座 修了証"
|
title: "Sharkey初心者講座 修了証"
|
||||||
description: "チュートリアルを完了した"
|
description: "チュートリアルを完了した"
|
||||||
|
_bubbleGameExplodingHead:
|
||||||
|
title: "🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを出した"
|
||||||
|
_bubbleGameDoubleExplodingHead:
|
||||||
|
title: "ダブル🤯"
|
||||||
|
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||||
|
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||||
|
|
||||||
_role:
|
_role:
|
||||||
new: "ロールの作成"
|
new: "ロールの作成"
|
||||||
|
@ -2205,6 +2219,7 @@ _profile:
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
favoritedNotes: "お気に入りにしたノート"
|
favoritedNotes: "お気に入りにしたノート"
|
||||||
|
clips: "クリップ"
|
||||||
followingList: "フォロー"
|
followingList: "フォロー"
|
||||||
muteList: "ミュート"
|
muteList: "ミュート"
|
||||||
blockingList: "ブロック"
|
blockingList: "ブロック"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.1.0.beta1",
|
"version": "2024.1.0.beta2",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -160,7 +160,6 @@ module.exports = {
|
||||||
testMatch: [
|
testMatch: [
|
||||||
"<rootDir>/test/unit/**/*.ts",
|
"<rootDir>/test/unit/**/*.ts",
|
||||||
"<rootDir>/src/**/*.test.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
|
// 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",
|
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./check_connect.js",
|
||||||
"build": "swc src -d built -D",
|
"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",
|
"watch:swc": "swc src -d built -D -w",
|
||||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||||
"watch": "node watch.mjs",
|
"watch": "node watch.mjs",
|
||||||
|
@ -21,11 +22,15 @@
|
||||||
"typecheck": "pnpm --filter megalodon build && tsc --noEmit",
|
"typecheck": "pnpm --filter megalodon build && tsc --noEmit",
|
||||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||||
"lint": "pnpm typecheck && pnpm eslint",
|
"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": "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-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
"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",
|
"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": "pnpm jest",
|
||||||
|
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage",
|
"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"
|
"generate-api-json": "node ./generate_api_json.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
@ -72,6 +77,8 @@
|
||||||
"@fastify/multipart": "8.0.0",
|
"@fastify/multipart": "8.0.0",
|
||||||
"@fastify/static": "6.12.0",
|
"@fastify/static": "6.12.0",
|
||||||
"@fastify/view": "8.2.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/common": "10.2.10",
|
||||||
"@nestjs/core": "10.2.10",
|
"@nestjs/core": "10.2.10",
|
||||||
"@nestjs/testing": "10.2.10",
|
"@nestjs/testing": "10.2.10",
|
||||||
|
@ -158,11 +165,9 @@
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"secure-json-parse": "2.7.0",
|
"secure-json-parse": "2.7.0",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
|
||||||
"slacc": "0.0.10",
|
"slacc": "0.0.10",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
|
||||||
"systeminformation": "5.21.20",
|
"systeminformation": "5.21.20",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
|
@ -179,6 +184,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
|
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.3.0",
|
||||||
"@simplewebauthn/typescript-types": "8.3.4",
|
"@simplewebauthn/typescript-types": "8.3.4",
|
||||||
"@swc/jest": "0.2.29",
|
"@swc/jest": "0.2.29",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
|
@ -228,9 +235,11 @@
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"execa": "8.0.1",
|
"execa": "8.0.1",
|
||||||
|
"fkill": "^9.0.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-mock": "29.7.0",
|
"jest-mock": "29.7.0",
|
||||||
"nodemon": "3.0.2",
|
"nodemon": "3.0.2",
|
||||||
|
"pid-port": "^1.0.0",
|
||||||
"simple-oauth2": "5.0.0"
|
"simple-oauth2": "5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Global, Inject, Module } from '@nestjs/common';
|
import { Global, Inject, Module } from '@nestjs/common';
|
||||||
import * as Redis from 'ioredis';
|
import * as Redis from 'ioredis';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
||||||
import { Config, loadConfig } from './config.js';
|
import { Config, loadConfig } from './config.js';
|
||||||
import { createPostgresDataSource } from './postgres.js';
|
import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
|
import { allSettled } from './misc/promise-tracker.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const $config: Provider = {
|
const $config: Provider = {
|
||||||
|
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
||||||
useFactory: (config: Config) => {
|
useFactory: (config: Config) => {
|
||||||
if (config.meilisearch) {
|
if (config.meilisearch) {
|
||||||
return new 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,
|
apiKey: config.meilisearch.apiKey,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential DB queries
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then disconnect from DB
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.db.destroy(),
|
this.db.destroy(),
|
||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
|
|
|
@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
||||||
'brainDiver',
|
'brainDiver',
|
||||||
'smashTestNotificationButton',
|
'smashTestNotificationButton',
|
||||||
'tutorialCompleted',
|
'tutorialCompleted',
|
||||||
|
'bubbleGameExplodingHead',
|
||||||
|
'bubbleGameDoubleExplodingHead',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@Injectable()
|
@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
|
@bindThis
|
||||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { sharpBmp } from 'sharp-read-bmp';
|
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -634,7 +634,7 @@ export class DriveService {
|
||||||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
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();
|
throw new DriveService.InvalidFileNameError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ export class EmailService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async validateEmailForAccount(emailAddress: string): Promise<{
|
public async validateEmailForAccount(emailAddress: string): Promise<{
|
||||||
available: boolean;
|
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();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
|
@ -173,6 +173,8 @@ export class EmailService {
|
||||||
if (meta.enableActiveEmailValidation) {
|
if (meta.enableActiveEmailValidation) {
|
||||||
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
||||||
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
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 {
|
} else {
|
||||||
validated = await validateEmail({
|
validated = await validateEmail({
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
|
@ -201,6 +203,8 @@ export class EmailService {
|
||||||
validated.reason === 'disposable' ? 'disposable' :
|
validated.reason === 'disposable' ? 'disposable' :
|
||||||
validated.reason === 'mx' ? 'mx' :
|
validated.reason === 'mx' ? 'mx' :
|
||||||
validated.reason === 'smtp' ? 'smtp' :
|
validated.reason === 'smtp' ? 'smtp' :
|
||||||
|
validated.reason === 'network' ? 'network' :
|
||||||
|
validated.reason === 'blacklist' ? 'blacklist' :
|
||||||
null,
|
null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -265,4 +269,67 @@ export class EmailService {
|
||||||
reason: null,
|
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 { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { isReply } from '@/misc/is-reply.js';
|
import { isReply } from '@/misc/is-reply.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -862,7 +863,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.relayService.deliverToRelays(user, noteActivity);
|
this.relayService.deliverToRelays(user, noteActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isMentioned: true,
|
isMentioned: true,
|
||||||
}).then(mentionsCount => {
|
}).then(mentionsCount => {
|
||||||
|
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
trackPromise(this.noteUnreadsRepository.countBy({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: true,
|
isSpecified: true,
|
||||||
}).then(specifiedCount => {
|
}).then(specifiedCount => {
|
||||||
|
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||||
// 全て既読になったイベントを発行
|
// 全て既読になったイベントを発行
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserListService } from '@/core/UserListService.js';
|
import { UserListService } from '@/core/UserListService.js';
|
||||||
import type { FilterUnionByProperty } from '@/types.js';
|
import type { FilterUnionByProperty } from '@/types.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
|
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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'],
|
notifieeId: MiUser['id'],
|
||||||
type: T,
|
type: T,
|
||||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { setTimeout } from 'node:timers/promises';
|
|
||||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||||
|
import { allSettled } from '@/misc/promise-tracker.js';
|
||||||
import type { Provider } from '@nestjs/common';
|
import type { Provider } from '@nestjs/common';
|
||||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
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> {
|
public async dispose(): Promise<void> {
|
||||||
if (process.env.NODE_ENV === 'test') {
|
// Wait for all potential queue jobs
|
||||||
// XXX:
|
await allSettled();
|
||||||
// Shutting down the existing connections causes errors on Jest as
|
// And then close all queues
|
||||||
// Misskey has asynchronous postgres/redis connections that are not
|
|
||||||
// awaited.
|
|
||||||
// Let's wait for some random time for them to finish.
|
|
||||||
await setTimeout(5000);
|
|
||||||
}
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.systemQueue.close(),
|
this.systemQueue.close(),
|
||||||
this.endedPollNotificationQueue.close(),
|
this.endedPollNotificationQueue.close(),
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '.
|
||||||
import type httpSignature from '@peertube/http-signature';
|
import type httpSignature from '@peertube/http-signature';
|
||||||
import type * as Bull from 'bullmq';
|
import type * as Bull from 'bullmq';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
|
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QueueService {
|
export class QueueService {
|
||||||
|
@ -75,11 +76,15 @@ export class QueueService {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
if (to == null) return null;
|
if (to == null) return null;
|
||||||
|
|
||||||
|
const contentBody = JSON.stringify(content);
|
||||||
|
const digest = ApRequestCreator.createDigest(contentBody);
|
||||||
|
|
||||||
const data: DeliverJobData = {
|
const data: DeliverJobData = {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
content,
|
content: contentBody,
|
||||||
|
digest,
|
||||||
to,
|
to,
|
||||||
isSharedInbox,
|
isSharedInbox,
|
||||||
};
|
};
|
||||||
|
@ -104,6 +109,8 @@ export class QueueService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map<string, boolean>) {
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
|
const contentBody = JSON.stringify(content);
|
||||||
|
const digest = ApRequestCreator.createDigest(contentBody);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
attempts: this.config.deliverJobMaxAttempts ?? 12,
|
||||||
|
@ -118,7 +125,8 @@ export class QueueService {
|
||||||
name: d[0],
|
name: d[0],
|
||||||
data: {
|
data: {
|
||||||
user,
|
user,
|
||||||
content,
|
content: contentBody,
|
||||||
|
digest,
|
||||||
to: d[0],
|
to: d[0],
|
||||||
isSharedInbox: d[1],
|
isSharedInbox: d[1],
|
||||||
} as DeliverJobData,
|
} 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
|
@bindThis
|
||||||
public createExportFavoritesJob(user: ThinUser) {
|
public createExportFavoritesJob(user: ThinUser) {
|
||||||
return this.dbQueue.add('exportFavorites', {
|
return this.dbQueue.add('exportFavorites', {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
|
||||||
const FALLBACK = '❤';
|
const FALLBACK = '❤';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||||
|
@ -280,7 +281,7 @@ export class ReactionService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
@ -328,7 +329,7 @@ export class ReactionService {
|
||||||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||||
}
|
}
|
||||||
dm.addFollowersRecipe();
|
dm.addFollowersRecipe();
|
||||||
dm.execute();
|
trackPromise(dm.execute());
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ class DeliverManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliver
|
// 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) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -256,7 +258,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
|
|
||||||
this.announceNote(actor, activity, targetUri);
|
await this.announceNote(actor, activity, targetUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -288,7 +290,7 @@ export class ApInboxService {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (err.isClientError) {
|
if (!err.isRetryable) {
|
||||||
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -373,7 +375,7 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(object)) {
|
if (isPost(object)) {
|
||||||
this.createNote(resolver, actor, object, false, activity);
|
await this.createNote(resolver, actor, object, false, activity);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
this.logger.warn(`Unknown type: ${getApType(object)}`);
|
||||||
}
|
}
|
||||||
|
@ -404,7 +406,7 @@ export class ApInboxService {
|
||||||
await this.apNoteService.createNote(note, resolver, silent);
|
await this.apNoteService.createNote(note, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && err.isClientError) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
return `skip ${err.statusCode}`;
|
return `skip ${err.statusCode}`;
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
|
|
|
@ -34,9 +34,9 @@ type PrivateKey = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ApRequestCreator {
|
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 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 = {
|
const request: Request = {
|
||||||
url: u.href,
|
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 {
|
static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }): Signed {
|
||||||
const u = new URL(args.url);
|
const u = new URL(args.url);
|
||||||
|
|
||||||
|
@ -145,8 +149,8 @@ export class ApRequestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown): Promise<void> {
|
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise<void> {
|
||||||
const body = JSON.stringify(object);
|
const body = typeof object === 'string' ? object : JSON.stringify(object);
|
||||||
|
|
||||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
|
@ -157,6 +161,7 @@ export class ApRequestService {
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
|
digest,
|
||||||
additionalHeaders: {
|
additionalHeaders: {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -221,7 +221,7 @@ export class ApNoteService {
|
||||||
return { status: 'ok', res };
|
return { status: 'ok', res };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
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,
|
color: channel.color,
|
||||||
isSensitive: channel.isSensitive,
|
isSensitive: channel.isSensitive,
|
||||||
allowRenoteToExternal: channel.allowRenoteToExternal,
|
allowRenoteToExternal: channel.allowRenoteToExternal,
|
||||||
|
userId: channel.userId,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
|
mentions: note.mentions && note.mentions.length > 0 ? note.mentions : undefined,
|
||||||
uri: note.uri ?? undefined,
|
uri: note.uri ?? undefined,
|
||||||
|
|
|
@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
||||||
const log = [] as any[];
|
const log = [] as any[];
|
||||||
|
|
||||||
ev.on('requestServerStatsLog', x => {
|
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 () => {
|
const tick = async () => {
|
||||||
|
|
|
@ -71,8 +71,11 @@ export default class Logger {
|
||||||
let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
|
let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
|
||||||
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
|
||||||
|
|
||||||
console.log(important ? chalk.bold(log) : log);
|
const args: unknown[] = [important ? chalk.bold(log) : log];
|
||||||
if (level === 'error' && data) console.log(data);
|
if (data != null) {
|
||||||
|
args.push(data);
|
||||||
|
}
|
||||||
|
console.log(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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 statusCode: number;
|
||||||
public statusMessage?: string;
|
public statusMessage?: string;
|
||||||
public isClientError: boolean;
|
public isClientError: boolean;
|
||||||
|
public isRetryable: boolean;
|
||||||
|
|
||||||
constructor(message: string, statusCode: number, statusMessage?: string) {
|
constructor(message: string, statusCode: number, statusMessage?: string) {
|
||||||
super(message);
|
super(message);
|
||||||
|
@ -14,5 +15,6 @@ export class StatusError extends Error {
|
||||||
this.statusCode = statusCode;
|
this.statusCode = statusCode;
|
||||||
this.statusMessage = statusMessage;
|
this.statusMessage = statusMessage;
|
||||||
this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
|
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;
|
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', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
|
@ -467,6 +490,23 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public verifymailAuthKey: string | null;
|
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', {
|
@Column('boolean', {
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -148,6 +148,10 @@ export const packedNoteSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
localOnly: {
|
localOnly: {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
|
||||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||||
|
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
||||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
||||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
||||||
|
@ -56,6 +57,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
ExportAccountDataProcessorService,
|
ExportAccountDataProcessorService,
|
||||||
ExportCustomEmojisProcessorService,
|
ExportCustomEmojisProcessorService,
|
||||||
ExportNotesProcessorService,
|
ExportNotesProcessorService,
|
||||||
|
ExportClipsProcessorService,
|
||||||
ExportFavoritesProcessorService,
|
ExportFavoritesProcessorService,
|
||||||
ExportFollowingProcessorService,
|
ExportFollowingProcessorService,
|
||||||
ExportMutingProcessorService,
|
ExportMutingProcessorService,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesP
|
||||||
import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.js';
|
import { ExportAccountDataProcessorService } from './processors/ExportAccountDataProcessorService.js';
|
||||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
||||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||||
|
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
||||||
|
@ -94,6 +95,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private exportAccountDataProcessorService: ExportAccountDataProcessorService,
|
private exportAccountDataProcessorService: ExportAccountDataProcessorService,
|
||||||
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
||||||
private exportNotesProcessorService: ExportNotesProcessorService,
|
private exportNotesProcessorService: ExportNotesProcessorService,
|
||||||
|
private exportClipsProcessorService: ExportClipsProcessorService,
|
||||||
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
||||||
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
||||||
private exportMutingProcessorService: ExportMutingProcessorService,
|
private exportMutingProcessorService: ExportMutingProcessorService,
|
||||||
|
@ -169,6 +171,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'exportAccountData': return this.exportAccountDataProcessorService.process(job);
|
case 'exportAccountData': return this.exportAccountDataProcessorService.process(job);
|
||||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||||
|
case 'exportClips': return this.exportClipsProcessorService.process(job);
|
||||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class DeliverProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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
|
// Update stats
|
||||||
this.federatedInstanceService.fetch(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
|
@ -111,7 +111,7 @@ export class DeliverProcessorService {
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
// 相手が閉鎖していることを明示しているため、配送停止する
|
// 相手が閉鎖していることを明示しているため、配送停止する
|
||||||
if (job.data.isSharedInbox && res.statusCode === 410) {
|
if (job.data.isSharedInbox && res.statusCode === 410) {
|
||||||
this.federatedInstanceService.fetch(host).then(i => {
|
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) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
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 Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`);
|
||||||
}
|
}
|
||||||
throw new Error(`Error in actor ${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) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (!res.isRetryable) {
|
||||||
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
throw new Bull.UnrecoverableError(`${res.statusCode} ${res.statusMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,9 @@ export type DeliverJobData = {
|
||||||
/** Actor */
|
/** Actor */
|
||||||
user: ThinUser;
|
user: ThinUser;
|
||||||
/** Activity */
|
/** Activity */
|
||||||
content: unknown;
|
content: string;
|
||||||
|
/** Digest header */
|
||||||
|
digest: string;
|
||||||
/** inbox URL to deliver */
|
/** inbox URL to deliver */
|
||||||
to: string;
|
to: string;
|
||||||
/** whether it is sharedInbox */
|
/** whether it is sharedInbox */
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { dirname } from 'node:path';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import rename from 'rename';
|
import rename from 'rename';
|
||||||
import sharp from 'sharp';
|
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 { Config } from '@/config.js';
|
||||||
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.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_exportFollowing from './endpoints/i/export-following.js';
|
||||||
import * as ep___i_exportMute from './endpoints/i/export-mute.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_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_exportFavorites from './endpoints/i/export-favorites.js';
|
||||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.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_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_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_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_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_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 };
|
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_exportFollowing,
|
||||||
$i_exportMute,
|
$i_exportMute,
|
||||||
$i_exportNotes,
|
$i_exportNotes,
|
||||||
|
$i_exportClips,
|
||||||
$i_exportFavorites,
|
$i_exportFavorites,
|
||||||
$i_exportUserLists,
|
$i_exportUserLists,
|
||||||
$i_exportAntennas,
|
$i_exportAntennas,
|
||||||
|
@ -1338,6 +1341,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
||||||
$i_exportFollowing,
|
$i_exportFollowing,
|
||||||
$i_exportMute,
|
$i_exportMute,
|
||||||
$i_exportNotes,
|
$i_exportNotes,
|
||||||
|
$i_exportClips,
|
||||||
$i_exportFavorites,
|
$i_exportFavorites,
|
||||||
$i_exportUserLists,
|
$i_exportUserLists,
|
||||||
$i_exportAntennas,
|
$i_exportAntennas,
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class SignupApiService {
|
||||||
'hcaptcha-response'?: string;
|
'hcaptcha-response'?: string;
|
||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
|
'm-captcha-response'?: string;
|
||||||
}
|
}
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
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) {
|
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||||
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { permissions } from 'misskey-js';
|
||||||
import type { Schema } from '@/misc/json-schema.js';
|
import type { Schema } from '@/misc/json-schema.js';
|
||||||
import { permissions } from 'misskey-js';
|
import { permissions } from 'misskey-js';
|
||||||
import { RolePolicies } from '@/core/RoleService.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_exportFollowing from './endpoints/i/export-following.js';
|
||||||
import * as ep___i_exportMute from './endpoints/i/export-mute.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_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_exportFavorites from './endpoints/i/export-favorites.js';
|
||||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.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-following', ep___i_exportFollowing],
|
||||||
['i/export-mute', ep___i_exportMute],
|
['i/export-mute', ep___i_exportMute],
|
||||||
['i/export-notes', ep___i_exportNotes],
|
['i/export-notes', ep___i_exportNotes],
|
||||||
|
['i/export-clips', ep___i_exportClips],
|
||||||
['i/export-favorites', ep___i_exportFavorites],
|
['i/export-favorites', ep___i_exportFavorites],
|
||||||
['i/export-user-lists', ep___i_exportUserLists],
|
['i/export-user-lists', ep___i_exportUserLists],
|
||||||
['i/export-antennas', ep___i_exportAntennas],
|
['i/export-antennas', ep___i_exportAntennas],
|
||||||
|
|
|
@ -45,6 +45,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
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: {
|
enableRecaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -174,6 +186,10 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
mcaptchaSecretKey: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
recaptchaSecretKey: {
|
recaptchaSecretKey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -299,6 +315,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
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: {
|
enableChartsForRemoteUser: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -476,6 +504,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||||
|
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
|
@ -508,6 +539,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
preservedUsernames: instance.preservedUsernames,
|
preservedUsernames: instance.preservedUsernames,
|
||||||
bubbleInstances: instance.bubbleInstances,
|
bubbleInstances: instance.bubbleInstances,
|
||||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||||
|
mcaptchaSecretKey: instance.mcaptchaSecretKey,
|
||||||
recaptchaSecretKey: instance.recaptchaSecretKey,
|
recaptchaSecretKey: instance.recaptchaSecretKey,
|
||||||
turnstileSecretKey: instance.turnstileSecretKey,
|
turnstileSecretKey: instance.turnstileSecretKey,
|
||||||
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
sensitiveMediaDetection: instance.sensitiveMediaDetection,
|
||||||
|
@ -543,6 +575,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
enableActiveEmailValidation: instance.enableActiveEmailValidation,
|
||||||
enableVerifymailApi: instance.enableVerifymailApi,
|
enableVerifymailApi: instance.enableVerifymailApi,
|
||||||
verifymailAuthKey: instance.verifymailAuthKey,
|
verifymailAuthKey: instance.verifymailAuthKey,
|
||||||
|
enableTruemailApi: instance.enableTruemailApi,
|
||||||
|
truemailInstance: instance.truemailInstance,
|
||||||
|
truemailAuthKey: instance.truemailAuthKey,
|
||||||
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
|
||||||
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
|
||||||
enableServerMachineStats: instance.enableServerMachineStats,
|
enableServerMachineStats: instance.enableServerMachineStats,
|
||||||
|
|
|
@ -65,6 +65,10 @@ export const paramDef = {
|
||||||
enableHcaptcha: { type: 'boolean' },
|
enableHcaptcha: { type: 'boolean' },
|
||||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
hcaptchaSecretKey: { 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' },
|
enableRecaptcha: { type: 'boolean' },
|
||||||
recaptchaSiteKey: { type: 'string', nullable: true },
|
recaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
recaptchaSecretKey: { type: 'string', nullable: true },
|
recaptchaSecretKey: { type: 'string', nullable: true },
|
||||||
|
@ -119,6 +123,9 @@ export const paramDef = {
|
||||||
enableActiveEmailValidation: { type: 'boolean' },
|
enableActiveEmailValidation: { type: 'boolean' },
|
||||||
enableVerifymailApi: { type: 'boolean' },
|
enableVerifymailApi: { type: 'boolean' },
|
||||||
verifymailAuthKey: { type: 'string', nullable: true },
|
verifymailAuthKey: { type: 'string', nullable: true },
|
||||||
|
enableTruemailApi: { type: 'boolean' },
|
||||||
|
truemailInstance: { type: 'string', nullable: true },
|
||||||
|
truemailAuthKey: { type: 'string', nullable: true },
|
||||||
enableChartsForRemoteUser: { type: 'boolean' },
|
enableChartsForRemoteUser: { type: 'boolean' },
|
||||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||||
enableServerMachineStats: { type: 'boolean' },
|
enableServerMachineStats: { type: 'boolean' },
|
||||||
|
@ -279,6 +286,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.hcaptchaSecretKey = ps.hcaptchaSecretKey;
|
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) {
|
if (ps.enableRecaptcha !== undefined) {
|
||||||
set.enableRecaptcha = ps.enableRecaptcha;
|
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) {
|
if (ps.enableChartsForRemoteUser !== undefined) {
|
||||||
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
antenna.isActive = true;
|
antenna.isActive = true;
|
||||||
antenna.lastUsedAt = new Date();
|
antenna.lastUsedAt = new Date();
|
||||||
this.antennasRepository.update(antenna.id, antenna);
|
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||||
|
|
||||||
if (needPublishEvent) {
|
if (needPublishEvent) {
|
||||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
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',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
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: {
|
enableRecaptcha: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -359,6 +371,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
approvalRequiredForSignup: instance.approvalRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
|
enableMcaptcha: instance.enableMcaptcha,
|
||||||
|
mcaptchaSiteKey: instance.mcaptchaSitekey,
|
||||||
|
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
enableAchievements: instance.enableAchievements,
|
enableAchievements: instance.enableAchievements,
|
||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
|
|
|
@ -21,6 +21,7 @@ class UserListChannel extends Channel {
|
||||||
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||||
private listUsersClock: NodeJS.Timeout;
|
private listUsersClock: NodeJS.Timeout;
|
||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
private withRenotes: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userListsRepository: UserListsRepository,
|
private userListsRepository: UserListsRepository,
|
||||||
|
@ -39,6 +40,7 @@ class UserListChannel extends Channel {
|
||||||
public async init(params: any) {
|
public async init(params: any) {
|
||||||
this.listId = params.listId as string;
|
this.listId = params.listId as string;
|
||||||
this.withFiles = params.withFiles ?? false;
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
|
|
||||||
// Check existence and owner
|
// Check existence and owner
|
||||||
const listExist = await this.userListsRepository.exist({
|
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がミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { summaly } from 'summaly';
|
import { summaly } from '@misskey-dev/summaly';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.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 cbor from 'cbor';
|
||||||
import * as OTPAuth from 'otpauth';
|
import * as OTPAuth from 'otpauth';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import { api, signup, startServer } from '../utils.js';
|
import { api, signup } from '../utils.js';
|
||||||
import type {
|
import type {
|
||||||
AuthenticationResponseJSON,
|
AuthenticationResponseJSON,
|
||||||
AuthenticatorAssertionResponseJSON,
|
AuthenticatorAssertionResponseJSON,
|
||||||
|
@ -19,12 +19,10 @@ import type {
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
RegistrationResponseJSON,
|
RegistrationResponseJSON,
|
||||||
} from '@simplewebauthn/typescript-types';
|
} from '@simplewebauthn/typescript-types';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('2要素認証', () => {
|
describe('2要素認証', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.MeSignup;
|
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const password = 'test';
|
const password = 'test';
|
||||||
|
@ -185,14 +183,9 @@ describe('2要素認証', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username, password });
|
alice = await signup({ username, password });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('が設定でき、OTPでログインできる。', async () => {
|
test('が設定でき、OTPでログインできる。', async () => {
|
||||||
const registerResponse = await api('/i/2fa/register', {
|
const registerResponse = await api('/i/2fa/register', {
|
||||||
password,
|
password,
|
||||||
|
|
|
@ -6,24 +6,20 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { inspect } from 'node:util';
|
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import {
|
import {
|
||||||
signup,
|
|
||||||
post,
|
|
||||||
userList,
|
|
||||||
page,
|
|
||||||
role,
|
|
||||||
startServer,
|
|
||||||
api,
|
api,
|
||||||
successfulApiCall,
|
|
||||||
failedApiCall,
|
failedApiCall,
|
||||||
uploadFile,
|
post,
|
||||||
|
role,
|
||||||
|
signup,
|
||||||
|
successfulApiCall,
|
||||||
testPaginationConsistency,
|
testPaginationConsistency,
|
||||||
|
uploadFile,
|
||||||
|
userList,
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
import type * as misskey from 'misskey-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 => {
|
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));
|
return selector(a).localeCompare(selector(b));
|
||||||
|
@ -37,7 +33,7 @@ describe('アンテナ', () => {
|
||||||
// - srcのenumにgroupが残っている
|
// - srcのenumにgroupが残っている
|
||||||
// - userGroupIdが残っている, isActiveがない
|
// - userGroupIdが残っている, isActiveがない
|
||||||
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
|
||||||
type User = misskey.entities.MeSignup;
|
type User = misskey.entities.SignupResponse;
|
||||||
type Note = misskey.entities.Note;
|
type Note = misskey.entities.Note;
|
||||||
|
|
||||||
// アンテナを作成できる最小のパラメタ
|
// アンテナを作成できる最小のパラメタ
|
||||||
|
@ -54,8 +50,6 @@ describe('アンテナ', () => {
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
let root: User;
|
let root: User;
|
||||||
let alice: User;
|
let alice: User;
|
||||||
let bob: User;
|
let bob: User;
|
||||||
|
@ -79,10 +73,6 @@ describe('アンテナ', () => {
|
||||||
let userMutingAlice: User;
|
let userMutingAlice: User;
|
||||||
let userMutedByAlice: User;
|
let userMutedByAlice: User;
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await startServer();
|
|
||||||
}, 1000 * 60 * 2);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
root = await signup({ username: 'root' });
|
root = await signup({ username: 'root' });
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
|
@ -136,10 +126,6 @@ describe('アンテナ', () => {
|
||||||
await api('mute/create', { userId: userMutedByAlice.id }, alice);
|
await api('mute/create', { userId: userMutedByAlice.id }, alice);
|
||||||
}, 1000 * 60 * 10);
|
}, 1000 * 60 * 10);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// テスト間で影響し合わないように毎回全部消す。
|
// テスト間で影響し合わないように毎回全部消す。
|
||||||
for (const user of [alice, bob]) {
|
for (const user of [alice, bob]) {
|
||||||
|
|
|
@ -6,33 +6,22 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, startServer } from '../utils.js';
|
import { api, post, signup } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('API visibility', () => {
|
describe('API visibility', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await startServer();
|
|
||||||
}, 1000 * 60 * 2);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Note visibility', () => {
|
describe('Note visibility', () => {
|
||||||
//#region vars
|
//#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を飛ばされる人 */
|
/** specified mentionでmentionを飛ばされる人 */
|
||||||
let target2: misskey.entities.MeSignup;
|
let target2: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
/** public-post */
|
/** public-post */
|
||||||
let pub: any;
|
let pub: any;
|
||||||
|
|
|
@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { IncomingMessage } from 'http';
|
import { IncomingMessage } from 'http';
|
||||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
|
import {
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
api,
|
||||||
|
connectStream,
|
||||||
|
createAppToken,
|
||||||
|
failedApiCall,
|
||||||
|
relativeFetch,
|
||||||
|
signup,
|
||||||
|
successfulApiCall,
|
||||||
|
uploadFile,
|
||||||
|
waitFire,
|
||||||
|
} from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('API', () => {
|
describe('API', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('General validation', () => {
|
describe('General validation', () => {
|
||||||
test('wrong type', async () => {
|
test('wrong type', async () => {
|
||||||
const res = await api('/test', {
|
const res = await api('/test', {
|
||||||
|
|
|
@ -6,29 +6,21 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, startServer } from '../utils.js';
|
import { api, post, signup } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Block', () => {
|
describe('Block', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
// alice blocks bob
|
// alice blocks bob
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Block作成', async () => {
|
test('Block作成', async () => {
|
||||||
const res = await api('/blocking/create', {
|
const res = await api('/blocking/create', {
|
||||||
userId: bob.id,
|
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 AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
|
||||||
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-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 { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
|
||||||
import {
|
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
|
||||||
signup,
|
|
||||||
post,
|
|
||||||
startServer,
|
|
||||||
api,
|
|
||||||
successfulApiCall,
|
|
||||||
failedApiCall,
|
|
||||||
ApiRequest,
|
|
||||||
hiddenNote,
|
|
||||||
} from '../utils.js';
|
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
describe('クリップ', () => {
|
describe('クリップ', () => {
|
||||||
type User = Packed<'User'>;
|
type User = Packed<'User'>;
|
||||||
type Note = Packed<'Note'>;
|
type Note = Packed<'Note'>;
|
||||||
type Clip = Packed<'Clip'>;
|
type Clip = Packed<'Clip'>;
|
||||||
|
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
let alice: User;
|
let alice: User;
|
||||||
let bob: User;
|
let bob: User;
|
||||||
let aliceNote: Note;
|
let aliceNote: Note;
|
||||||
|
@ -145,7 +133,6 @@ describe('クリップ', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
|
|
||||||
|
@ -160,10 +147,6 @@ describe('クリップ', () => {
|
||||||
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
|
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
// テスト間で影響し合わないように毎回全部消す。
|
// テスト間で影響し合わないように毎回全部消す。
|
||||||
for (const user of [alice, bob]) {
|
for (const user of [alice, bob]) {
|
||||||
|
|
|
@ -10,30 +10,22 @@ import * as assert from 'assert';
|
||||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||||
import { Blob } from 'node-fetch';
|
import { Blob } from 'node-fetch';
|
||||||
import { MiUser } from '@/models/_.js';
|
import { MiUser } from '@/models/_.js';
|
||||||
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
|
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
let bob: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let dave: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
|
||||||
let dave: misskey.entities.MeSignup;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
dave = await signup({ username: 'dave' });
|
dave = await signup({ username: 'dave' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('signup', () => {
|
describe('signup', () => {
|
||||||
test('不正なユーザー名でアカウントが作成できない', async () => {
|
test('不正なユーザー名でアカウントが作成できない', async () => {
|
||||||
const res = await api('signup', {
|
const res = await api('signup', {
|
||||||
|
@ -710,6 +702,18 @@ describe('Endpoints', () => {
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('間違ったIDで怒られる', async () => {
|
||||||
const res = await api('/drive/files/update', {
|
const res = await api('/drive/files/update', {
|
||||||
fileId: 'kyoppie',
|
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';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
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 { SimpleGetResponse } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
// Request Accept
|
// Request Accept
|
||||||
|
@ -23,9 +22,7 @@ const HTML = 'text/html; charset=utf-8';
|
||||||
const JSON_UTF8 = 'application/json; charset=utf-8';
|
const JSON_UTF8 = 'application/json; charset=utf-8';
|
||||||
|
|
||||||
describe('Webリソース', () => {
|
describe('Webリソース', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
|
||||||
let aliceUploadedFile: any;
|
let aliceUploadedFile: any;
|
||||||
let alicesPost: any;
|
let alicesPost: any;
|
||||||
let alicePage: any;
|
let alicePage: any;
|
||||||
|
@ -34,7 +31,7 @@ describe('Webリソース', () => {
|
||||||
let aliceGalleryPost: any;
|
let aliceGalleryPost: any;
|
||||||
let aliceChannel: any;
|
let aliceChannel: any;
|
||||||
|
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
path: string,
|
path: string,
|
||||||
|
@ -79,7 +76,6 @@ describe('Webリソース', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
aliceUploadedFile = await uploadFile(alice);
|
aliceUploadedFile = await uploadFile(alice);
|
||||||
alicesPost = await post(alice, {
|
alicesPost = await post(alice, {
|
||||||
|
@ -96,10 +92,6 @@ describe('Webリソース', () => {
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
{ path: '/', type: HTML },
|
{ path: '/', type: HTML },
|
||||||
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
|
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
|
||||||
|
|
|
@ -6,26 +6,18 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, startServer, simpleGet } from '../utils.js';
|
import { api, signup, simpleGet } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('FF visibility', () => {
|
describe('FF visibility', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
let bob: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.MeSignup;
|
|
||||||
let bob: misskey.entities.MeSignup;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
|
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
|
||||||
await api('/i/update', {
|
await api('/i/update', {
|
||||||
followingVisibility: 'public',
|
followingVisibility: 'public',
|
||||||
|
|
|
@ -3,35 +3,35 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { loadConfig } from '@/config.js';
|
import { loadConfig } from '@/config.js';
|
||||||
import { MiUser, UsersRepository } from '@/models/_.js';
|
import { MiUser, UsersRepository } from '@/models/_.js';
|
||||||
import { jobQueue } from '@/boot/common.js';
|
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js';
|
import { jobQueue } from '@/boot/common.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Account Move', () => {
|
describe('Account Move', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
let jq: INestApplicationContext;
|
let jq: INestApplicationContext;
|
||||||
let url: URL;
|
let url: URL;
|
||||||
|
|
||||||
let root: any;
|
let root: any;
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let dave: misskey.entities.MeSignup;
|
let dave: misskey.entities.SignupResponse;
|
||||||
let eve: misskey.entities.MeSignup;
|
let eve: misskey.entities.SignupResponse;
|
||||||
let frank: misskey.entities.MeSignup;
|
let frank: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let Users: UsersRepository;
|
let Users: UsersRepository;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
jq = await jobQueue();
|
jq = await jobQueue();
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
url = new URL(config.url);
|
url = new URL(config.url);
|
||||||
const connection = await initTestDb(false);
|
const connection = await initTestDb(false);
|
||||||
|
@ -46,7 +46,7 @@ describe('Account Move', () => {
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await Promise.all([app.close(), jq.close()]);
|
await jq.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Create Alias', () => {
|
describe('Create Alias', () => {
|
||||||
|
|
|
@ -6,29 +6,21 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
|
import { api, post, react, signup, waitFire } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Mute', () => {
|
describe('Mute', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
// alice mutes carol
|
// alice mutes carol
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ミュート作成', async () => {
|
test('ミュート作成', async () => {
|
||||||
const res = await api('/mute/create', {
|
const res = await api('/mute/create', {
|
||||||
userId: carol.id,
|
userId: carol.id,
|
||||||
|
|
|
@ -6,20 +6,9 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { relativeFetch, startServer } from '../utils.js';
|
import { relativeFetch } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
describe('nodeinfo', () => {
|
describe('nodeinfo', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await startServer();
|
|
||||||
}, 1000 * 60 * 2);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('nodeinfo 2.1', async () => {
|
test('nodeinfo 2.1', async () => {
|
||||||
const res = await relativeFetch('nodeinfo/2.1');
|
const res = await relativeFetch('nodeinfo/2.1');
|
||||||
assert.ok(res.ok);
|
assert.ok(res.ok);
|
||||||
|
|
|
@ -8,29 +8,22 @@ process.env.NODE_ENV = 'test';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { MiNote } from '@/models/Note.js';
|
import { MiNote } from '@/models/Note.js';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||||
import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js';
|
import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Note', () => {
|
describe('Note', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
let Notes: any;
|
let Notes: any;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
const connection = await initTestDb(true);
|
const connection = await initTestDb(true);
|
||||||
Notes = connection.getRepository(MiNote);
|
Notes = connection.getRepository(MiNote);
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('投稿できる', async () => {
|
test('投稿できる', async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: 'test',
|
text: 'test',
|
||||||
|
|
|
@ -11,13 +11,18 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
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 pkceChallenge from 'pkce-challenge';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify';
|
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
|
||||||
import { api, port, signup, startServer } from '../utils.js';
|
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
const host = `http://127.0.0.1:${port}`;
|
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), {
|
return fetch(new URL('/oauth/decision', host), {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: new URLSearchParams({
|
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());
|
const { transactionId } = getMeta(await response.text());
|
||||||
assert.ok(transactionId);
|
assert.ok(transactionId);
|
||||||
|
|
||||||
return await fetchDecision(transactionId, user, { cancel });
|
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 client = new AuthorizationCode(clientConfig);
|
||||||
|
|
||||||
const response = await fetch(client.authorizeURL({
|
const response = await fetch(client.authorizeURL({
|
||||||
|
@ -147,16 +152,14 @@ async function assertDirectError(response: Response, status: number, error: stri
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('OAuth', () => {
|
describe('OAuth', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
let fastify: FastifyInstance;
|
let fastify: FastifyInstance;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let sender: (reply: FastifyReply) => void;
|
let sender: (reply: FastifyReply) => void;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
|
|
||||||
|
@ -168,7 +171,7 @@ describe('OAuth', () => {
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '';
|
await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' });
|
||||||
sender = (reply): void => {
|
sender = (reply): void => {
|
||||||
reply.send(`
|
reply.send(`
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -180,7 +183,6 @@ describe('OAuth', () => {
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fastify.close();
|
await fastify.close();
|
||||||
await app.close();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Full flow', async () => {
|
test('Full flow', async () => {
|
||||||
|
@ -881,7 +883,7 @@ describe('OAuth', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Disallow loopback', async () => {
|
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 client = new AuthorizationCode(clientConfig);
|
||||||
const response = await fetch(client.authorizeURL({
|
const response = await fetch(client.authorizeURL({
|
||||||
|
|
|
@ -6,29 +6,21 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js';
|
import { api, post, signup, sleep, waitFire } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Renote Mute', () => {
|
describe('Renote Mute', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
// alice mutes carol
|
// alice mutes carol
|
||||||
let alice: misskey.entities.MeSignup;
|
let alice: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
let bob: misskey.entities.SignupResponse;
|
||||||
let carol: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ミュート作成', async () => {
|
test('ミュート作成', async () => {
|
||||||
const res = await api('/renote-mute/create', {
|
const res = await api('/renote-mute/create', {
|
||||||
userId: carol.id,
|
userId: carol.id,
|
||||||
|
|
|
@ -8,12 +8,10 @@ process.env.NODE_ENV = 'test';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import { MiFollowing } from '@/models/Following.js';
|
import { MiFollowing } from '@/models/Following.js';
|
||||||
import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js';
|
import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Streaming', () => {
|
describe('Streaming', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
let Followings: any;
|
let Followings: any;
|
||||||
|
|
||||||
const follow = async (follower: any, followee: any) => {
|
const follow = async (follower: any, followee: any) => {
|
||||||
|
@ -32,15 +30,15 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
describe('Streaming', () => {
|
describe('Streaming', () => {
|
||||||
// Local users
|
// Local users
|
||||||
let ayano: misskey.entities.MeSignup;
|
let ayano: misskey.entities.SignupResponse;
|
||||||
let kyoko: misskey.entities.MeSignup;
|
let kyoko: misskey.entities.SignupResponse;
|
||||||
let chitose: misskey.entities.MeSignup;
|
let chitose: misskey.entities.SignupResponse;
|
||||||
let kanako: misskey.entities.MeSignup;
|
let kanako: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
// Remote users
|
// Remote users
|
||||||
let akari: misskey.entities.MeSignup;
|
let akari: misskey.entities.SignupResponse;
|
||||||
let chinatsu: misskey.entities.MeSignup;
|
let chinatsu: misskey.entities.SignupResponse;
|
||||||
let takumi: misskey.entities.MeSignup;
|
let takumi: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let kyokoNote: any;
|
let kyokoNote: any;
|
||||||
let kanakoNote: any;
|
let kanakoNote: any;
|
||||||
|
@ -48,7 +46,6 @@ describe('Streaming', () => {
|
||||||
let list: any;
|
let list: any;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
const connection = await initTestDb(true);
|
const connection = await initTestDb(true);
|
||||||
Followings = connection.getRepository(MiFollowing);
|
Followings = connection.getRepository(MiFollowing);
|
||||||
|
|
||||||
|
@ -95,10 +92,6 @@ describe('Streaming', () => {
|
||||||
}, chitose);
|
}, chitose);
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Events', () => {
|
describe('Events', () => {
|
||||||
test('mention event', async () => {
|
test('mention event', async () => {
|
||||||
const fired = await waitFire(
|
const fired = await waitFire(
|
||||||
|
|
|
@ -6,28 +6,20 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, connectStream, startServer } from '../utils.js';
|
import { api, connectStream, post, signup } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('Note thread mute', () => {
|
describe('Note thread mute', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
let bob: misskey.entities.SignupResponse;
|
||||||
let alice: misskey.entities.MeSignup;
|
let carol: misskey.entities.SignupResponse;
|
||||||
let bob: misskey.entities.MeSignup;
|
|
||||||
let carol: misskey.entities.MeSignup;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
bob = await signup({ username: 'bob' });
|
bob = await signup({ username: 'bob' });
|
||||||
carol = await signup({ username: 'carol' });
|
carol = await signup({ username: 'carol' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
|
test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
|
||||||
const bobNote = await post(bob, { text: '@alice @carol root note' });
|
const bobNote = await post(bob, { text: '@alice @carol root note' });
|
||||||
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
|
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
|
||||||
|
|
|
@ -6,12 +6,8 @@
|
||||||
// How to run:
|
// How to run:
|
||||||
// pnpm jest -- e2e/timelines.ts
|
// 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 * as assert from 'assert';
|
||||||
import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js';
|
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
function genHost() {
|
function genHost() {
|
||||||
return randomString() + '.example.com';
|
return randomString() + '.example.com';
|
||||||
|
@ -21,16 +17,6 @@ function waitForPushToTl() {
|
||||||
return sleep(500);
|
return sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await startServer();
|
|
||||||
}, 1000 * 60 * 2);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Timelines', () => {
|
describe('Timelines', () => {
|
||||||
describe('Home TL', () => {
|
describe('Home TL', () => {
|
||||||
test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
|
test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
|
||||||
|
@ -334,8 +320,9 @@ describe('Timelines', () => {
|
||||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
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 api('/following/create', { userId: bob.id }, alice);
|
||||||
await sleep(1000);
|
|
||||||
const bobNote = await post(bob, { text: 'hi' });
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
@ -348,8 +335,9 @@ describe('Timelines', () => {
|
||||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
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 api('/following/create', { userId: bob.id }, alice);
|
||||||
await sleep(1000);
|
|
||||||
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
@ -762,8 +750,9 @@ describe('Timelines', () => {
|
||||||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
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 api('/following/create', { userId: bob.id }, alice);
|
||||||
await sleep(1000);
|
|
||||||
const bobNote = await post(bob, { text: 'hi' });
|
const bobNote = await post(bob, { text: 'hi' });
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
@ -776,8 +765,9 @@ describe('Timelines', () => {
|
||||||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
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 api('/following/create', { userId: bob.id }, alice);
|
||||||
await sleep(1000);
|
|
||||||
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||||
|
|
||||||
await waitForPushToTl();
|
await waitForPushToTl();
|
||||||
|
|
|
@ -6,20 +6,16 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { signup, api, post, uploadUrl, startServer } from '../utils.js';
|
import { api, post, signup, uploadUrl } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('users/notes', () => {
|
describe('users/notes', () => {
|
||||||
let app: INestApplicationContext;
|
let alice: misskey.entities.SignupResponse;
|
||||||
|
|
||||||
let alice: misskey.entities.MeSignup;
|
|
||||||
let jpgNote: any;
|
let jpgNote: any;
|
||||||
let pngNote: any;
|
let pngNote: any;
|
||||||
let jpgPngNote: any;
|
let jpgPngNote: any;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
alice = await signup({ username: 'alice' });
|
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 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');
|
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);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async() => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('withFiles', async () => {
|
test('withFiles', async () => {
|
||||||
const res = await api('/users/notes', {
|
const res = await api('/users/notes', {
|
||||||
userId: alice.id,
|
userId: alice.id,
|
||||||
|
|
|
@ -8,20 +8,8 @@ process.env.NODE_ENV = 'test';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import {
|
|
||||||
signup,
|
|
||||||
post,
|
|
||||||
page,
|
|
||||||
role,
|
|
||||||
startServer,
|
|
||||||
api,
|
|
||||||
successfulApiCall,
|
|
||||||
failedApiCall,
|
|
||||||
uploadFile,
|
|
||||||
} from '../utils.js';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
|
|
||||||
describe('ユーザー', () => {
|
describe('ユーザー', () => {
|
||||||
// エンティティとしてのユーザーを主眼においたテストを記述する
|
// エンティティとしてのユーザーを主眼においたテストを記述する
|
||||||
|
@ -188,8 +176,6 @@ describe('ユーザー', () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let app: INestApplicationContext;
|
|
||||||
|
|
||||||
let root: User;
|
let root: User;
|
||||||
let alice: User;
|
let alice: User;
|
||||||
let aliceNote: misskey.entities.Note;
|
let aliceNote: misskey.entities.Note;
|
||||||
|
@ -233,10 +219,6 @@ describe('ユーザー', () => {
|
||||||
let userFollowRequesting: User;
|
let userFollowRequesting: User;
|
||||||
let userFollowRequested: User;
|
let userFollowRequested: User;
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
app = await startServer();
|
|
||||||
}, 1000 * 60 * 2);
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
root = await signup({ username: 'root' });
|
root = await signup({ username: 'root' });
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
|
@ -324,10 +306,6 @@ describe('ユーザー', () => {
|
||||||
await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting);
|
await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting);
|
||||||
}, 1000 * 60 * 10);
|
}, 1000 * 60 * 10);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
alice = {
|
alice = {
|
||||||
...alice,
|
...alice,
|
||||||
|
|
|
@ -6,24 +6,16 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { host, origin, relativeFetch, signup, startServer } from '../utils.js';
|
import { host, origin, relativeFetch, signup } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
describe('.well-known', () => {
|
describe('.well-known', () => {
|
||||||
let app: INestApplicationContext;
|
|
||||||
let alice: misskey.entities.User;
|
let alice: misskey.entities.User;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
|
||||||
|
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('nodeinfo', async () => {
|
test('nodeinfo', async () => {
|
||||||
const res = await relativeFetch('.well-known/nodeinfo');
|
const res = await relativeFetch('.well-known/nodeinfo');
|
||||||
assert.ok(res.ok);
|
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 { MetaService } from '@/core/MetaService.js';
|
||||||
import type { UtilityService } from '@/core/UtilityService.js';
|
import type { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.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 MockResponse = {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
|
@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { AnnouncementService } from '@/core/AnnouncementService.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 { DI } from '@/di-symbols.js';
|
||||||
import { genAidx } from '@/misc/id/aidx.js';
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
|
@ -6,7 +6,13 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import { Test } from '@nestjs/testing';
|
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 { mockClient } from 'aws-sdk-client-mock';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
|
|
|
@ -55,7 +55,8 @@ describe('FetchInstanceMetadataService', () => {
|
||||||
return { fetch: jest.fn() };
|
return { fetch: jest.fn() };
|
||||||
} else if (token === DI.redis) {
|
} else if (token === DI.redis) {
|
||||||
return mockRedis;
|
return mockRedis;
|
||||||
}})
|
}
|
||||||
|
})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { ModuleMocker } from 'jest-mock';
|
import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
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 { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||||
//import { DI } from '@/di-symbols.js';
|
//import { DI } from '@/di-symbols.js';
|
||||||
|
|
|
@ -6,15 +6,13 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
import { ModuleMocker } from 'jest-mock';
|
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import type { MetasRepository } from '@/models/_.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import type { DataSource } from 'typeorm';
|
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
|
import type { DataSource } from 'typeorm';
|
||||||
|
|
||||||
describe('MetaService', () => {
|
describe('MetaService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { Test } from '@nestjs/testing';
|
||||||
import * as lolex from '@sinonjs/fake-timers';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { RoleService } from '@/core/RoleService.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 { DI } from '@/di-symbols.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { genAidx } from '@/misc/id/aidx.js';
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
|
|
|
@ -6,7 +6,13 @@
|
||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import { Test } from '@nestjs/testing';
|
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 { mockClient } from 'aws-sdk-client-mock';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ulid } from 'ulid';
|
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 { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js';
|
||||||
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
|
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
|
||||||
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
|
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.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', () => {
|
describe('misc:id', () => {
|
||||||
test('aid', () => {
|
test('aid', () => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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';
|
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||||
|
|
||||||
describe('misc:content-disposition', () => {
|
describe('misc:content-disposition', () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import * as assert from 'node:assert';
|
import * as assert from 'node:assert';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { isAbsolute, basename } from 'node:path';
|
import { basename, isAbsolute } from 'node:path';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import WebSocket, { ClientOptions } from 'ws';
|
import WebSocket, { ClientOptions } from 'ws';
|
||||||
|
@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js';
|
||||||
import { loadConfig } from '../src/config.js';
|
import { loadConfig } from '../src/config.js';
|
||||||
import type * as misskey from 'misskey-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 {
|
interface UserToken {
|
||||||
token: string;
|
token: string;
|
||||||
|
@ -68,7 +68,11 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
|
||||||
return res.body;
|
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 bodyAuth: Record<string, string> = {};
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -275,7 +279,11 @@ interface UploadOptions {
|
||||||
* Upload file
|
* Upload file
|
||||||
* @param user User
|
* @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
|
const absPath = path == null
|
||||||
? new URL('resources/Lenna.jpg', import.meta.url)
|
? new URL('resources/Lenna.jpg', import.meta.url)
|
||||||
: isAbsolute(path.toString())
|
: isAbsolute(path.toString())
|
||||||
|
@ -426,8 +434,8 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
|
||||||
];
|
];
|
||||||
|
|
||||||
const body =
|
const body =
|
||||||
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
|
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
|
||||||
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
|
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
|
||||||
null;
|
null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -557,3 +565,34 @@ export function sleep(msec: number) {
|
||||||
}, msec);
|
}, 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 |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue