1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-30 15:58:19 +09:00

Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2024-01-10 17:30:21 +09:00
commit d6e8ed0197
438 changed files with 5965 additions and 2373 deletions

View File

@ -2,3 +2,4 @@
POSTGRES_PASSWORD=example-cherrypick-pass POSTGRES_PASSWORD=example-cherrypick-pass
POSTGRES_USER=example-cherrypick-user POSTGRES_USER=example-cherrypick-user
POSTGRES_DB=cherrypick POSTGRES_DB=cherrypick
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

View File

@ -17,16 +17,32 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
# PNPM has an issue with dependabot. See: open-pull-requests-limit: 10
# https://github.com/dependabot/dependabot-core/issues/7258 # List dependencies required to be updated together, sharing the same version numbers.
# https://github.com/pnpm/pnpm/issues/6530 # Those who simply have the common owner (e.g. @fastify) don't need to be listed.
# TODO: Restore this when the issue is solved
open-pull-requests-limit: 0
groups: groups:
swc: aws-sdk:
patterns: patterns:
- "@swc/*" - "@aws-sdk/*"
bull-board:
patterns:
- "@bull-board/*"
nestjs:
patterns:
- "@nestjs/*"
slacc:
patterns:
- "slacc-*"
storybook: storybook:
patterns: patterns:
- "storybook*" - "storybook*"
- "@storybook/*" - "@storybook/*"
swc-core:
patterns:
- "@swc/core*"
typescript-eslint:
patterns:
- "@typescript-eslint/*"
tensorflow:
patterns:
- "@tensorflow/*"

View File

@ -1,6 +1,12 @@
name: API report (cherrypick.js) name: API report (cherrypick.js)
on: [push, pull_request] on:
push:
paths:
- packages/cherrypick-js/**
pull_request:
paths:
- packages/cherrypick-js/**
jobs: jobs:
report: report:

View File

@ -5,7 +5,19 @@ on:
branches: branches:
- master - master
- develop - develop
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/cherrypick-js/**
- packages/shared/.eslintrc.js
pull_request: pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/cherrypick-js/**
- packages/shared/.eslintrc.js
jobs: jobs:
pnpm_install: pnpm_install:

View File

@ -5,10 +5,18 @@ on:
branches: branches:
- master - master
- develop - develop
paths:
- packages/backend/**
# for permissions
- packages/cherrypick-js/**
pull_request: pull_request:
paths:
- packages/backend/**
# for permissions
- packages/cherrypick-js/**
jobs: jobs:
jest: unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -51,9 +59,59 @@ jobs:
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Test - name: Test
run: pnpm jest-and-coverage run: pnpm --filter backend test-and-coverage
- name: Upload Coverage - name: Upload to Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json files: ./packages/backend/coverage/coverage-final.json
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.10.0]
services:
postgres:
image: postgres:15
ports:
- 54312:5432
env:
POSTGRES_DB: test-cherrypick
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
ports:
- 56312:6379
steps:
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .github/cherrypick/test.yml .config
- name: Build
run: pnpm build
- name: Test
run: pnpm --filter backend test-and-coverage:e2e
- name: Upload to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json

View File

@ -6,8 +6,12 @@ name: Test (cherrypick.js)
on: on:
push: push:
branches: [ develop ] branches: [ develop ]
paths:
- packages/cherrypick-js/**
pull_request: pull_request:
branches: [ develop ] branches: [ develop ]
paths:
- packages/cherrypick-js/**
jobs: jobs:
test: test:

View File

@ -5,7 +5,20 @@ on:
branches: branches:
- master - master
- develop - develop
paths:
- packages/frontend/**
# for permissions
- packages/cherrypick-js/**
# for e2e
- packages/backend/**
pull_request: pull_request:
paths:
- packages/frontend/**
# for permissions
- packages/cherrypick-js/**
# for e2e
- packages/backend/**
jobs: jobs:
vitest: vitest:

47
.github/workflows/validate-api-json.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Test (backend)
on:
push:
branches:
- master
- develop
paths:
- packages/backend/**
pull_request:
paths:
- packages/backend/**
jobs:
validate-api-json:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.10.0]
steps:
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install swagger-cli
run: npm i -g swagger-cli
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .config/example.yml .config/default.yml
- name: Build and generate
run: pnpm build && pnpm --filter backend generate-api-json
- name: Validation
run: swagger-cli validate ./packages/backend/built/api.json

1
.gitignore vendored
View File

@ -41,6 +41,7 @@ docker-compose.yml
# cherrypick # cherrypick
/build /build
built built
built-test
/data /data
/.cache-loader /.cache-loader
/db /db

View File

@ -12,11 +12,34 @@
--> -->
## 202x.x.x (Unreleased)
### General
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
### Client
- Feat: 新しいゲームを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
### Server
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
## 2023.12.2 ## 2023.12.2
### General ### General
- v2023.12.1でDockerを利用してサーバーを起動できない問題を修正 - v2023.12.1でDockerを利用してサーバーを起動できない問題を修正
### Client
- Enhance: 検索画面においてEnterキー押下で検索できるように
## 2023.12.1 ## 2023.12.1
### Note ### Note
@ -124,7 +147,6 @@
- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように - Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正 - Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正 - Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
- Enhance: 検索画面においてEnterキー押下で検索できるように
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように

View File

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

View File

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

19
locales/index.d.ts vendored
View File

@ -462,6 +462,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;
@ -771,6 +776,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;
@ -1297,6 +1303,9 @@ export interface Locale {
"decorate": string; "decorate": string;
"addMfmFunction": string; "addMfmFunction": string;
"enableQuickAddMfmFunction": string; "enableQuickAddMfmFunction": string;
"bubbleGame": string;
"sfx": string;
"soundWillBePlayed": string;
"showUnreadNotificationsCount": string; "showUnreadNotificationsCount": string;
"showCatOnly": string; "showCatOnly": string;
"additionalPermissionsForFlash": string; "additionalPermissionsForFlash": string;
@ -1874,6 +1883,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": {
@ -2575,6 +2593,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;

View File

@ -459,6 +459,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: "サイトキー"
@ -768,6 +773,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
other: "その他" other: "その他"
regenerateLoginToken: "ログイントークンを再生成" regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL" fileIdOrUrl: "ファイルIDまたはURL"
behavior: "動作" behavior: "動作"
@ -1294,6 +1300,9 @@ seasonalScreenEffect: "季節に応じた画面の演出"
decorate: "デコる" decorate: "デコる"
addMfmFunction: "装飾を追加" addMfmFunction: "装飾を追加"
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
bubbleGame: "バブルゲーム"
sfx: "効果音"
soundWillBePlayed: "サウンドが再生されます"
showUnreadNotificationsCount: "未読の通知の数を表示する" showUnreadNotificationsCount: "未読の通知の数を表示する"
showCatOnly: "キャット付きのみ" showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可" additionalPermissionsForFlash: "Playへの追加許可"
@ -1783,6 +1792,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "CherryPick初心者講座 修了証" title: "CherryPick初心者講座 修了証"
description: "チュートリアルを完了した" description: "チュートリアルを完了した"
_bubbleGameExplodingHead:
title: "🤯"
description: "バブルゲームで最も大きいモノを出した"
_bubbleGameDoubleExplodingHead:
title: "ダブル🤯"
description: "バブルゲームで最も大きいモを2つ同時に出した"
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
@ -2474,6 +2490,7 @@ _profile:
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート" favoritedNotes: "お気に入りにしたノート"
clips: "クリップ"
followingList: "フォロー" followingList: "フォロー"
muteList: "ミュート" muteList: "ミュート"
blockingList: "ブロック" blockingList: "ブロック"

View File

@ -19,7 +19,7 @@
"build-assets": "node ./scripts/build-assets.mjs", "build-assets": "node ./scripts/build-assets.mjs",
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
"build-storybook": "pnpm --filter frontend build-storybook", "build-storybook": "pnpm --filter frontend build-storybook",
"build-cherrypick-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build", "build-cherrypick-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/cherrypick-js/generator/api.json && pnpm --filter cherrypick-js update-autogen-code && pnpm --filter cherrypick-js build && pnpm --filter cherrypick-js api",
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
"start:docker": "pnpm check:connect && cd packages/backend && exec node ./built/boot/entry.js", "start:docker": "pnpm check:connect && cd packages/backend && exec node ./built/boot/entry.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",

View File

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

View 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",
],
};

View 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",
],
};

View File

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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"`);
}
}

View File

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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"`);
}
}

View File

@ -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": "tsc --noEmit", "typecheck": "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",
"schema:sync": "pnpm typeorm schema:sync -d ormconfig.js" "schema:sync": "pnpm typeorm schema:sync -d ormconfig.js"
}, },
@ -77,6 +82,8 @@
"@fastify/view": "8.2.0", "@fastify/view": "8.2.0",
"@google-cloud/logging": "^10.5.0", "@google-cloud/logging": "^10.5.0",
"@google-cloud/translate": "^7.2.1", "@google-cloud/translate": "^7.2.1",
"@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",
@ -161,12 +168,10 @@
"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",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.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",
@ -182,6 +187,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",
@ -230,9 +237,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"
} }
} }

View File

@ -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 process from 'node:process'; import process from 'node:process';
import { Global, Inject, Module } from '@nestjs/common'; import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
@ -14,6 +13,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 = {
@ -35,7 +35,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 {
@ -120,17 +120,12 @@ export class GlobalModule implements OnApplicationShutdown {
@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,
@Inject(DI.redisForJobQueue) private redisForJobQueue: Redis.Redis, @Inject(DI.redisForJobQueue) private redisForJobQueue: 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(),

View File

@ -88,6 +88,8 @@ export const ACHIEVEMENT_TYPES = [
'brainDiver', 'brainDiver',
'smashTestNotificationButton', 'smashTestNotificationButton',
'tutorialCompleted', 'tutorialCompleted',
'bubbleGameExplodingHead',
'bubbleGameDoubleExplodingHead',
] as const; ] as const;
@Injectable() @Injectable()

View File

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

View File

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

View File

@ -155,7 +155,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();
@ -172,6 +172,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,
@ -200,6 +202,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,
}; };
} }
@ -264,4 +268,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',
};
}
}
} }

View File

@ -60,6 +60,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';
@ -701,7 +702,7 @@ export class NoteCreateService implements OnApplicationShutdown {
this.relayService.deliverToRelays(user, noteActivity); this.relayService.deliverToRelays(user, noteActivity);
} }
dm.execute(); trackPromise(dm.execute());
})(); })();
} }
//#endregion //#endregion

View File

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

View File

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

View File

@ -3,13 +3,13 @@
* 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 * as Redis from 'ioredis'; import * as Redis from 'ioredis';
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';
@ -107,14 +107,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(),

View File

@ -17,6 +17,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js';
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 { 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],
}, },
@ -175,6 +183,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', {

View File

@ -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;
@ -268,7 +269,7 @@ export class ReactionService {
} }
} }
dm.execute(); trackPromise(dm.execute());
} }
//#endregion //#endregion
} }
@ -316,7 +317,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
} }

View File

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

View File

@ -106,6 +106,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;
} }
} }
} }
@ -290,7 +292,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
@ -325,7 +327,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;
} }
@ -416,7 +418,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)}`);
} }
@ -447,7 +449,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;

View File

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

View File

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

View File

@ -370,6 +370,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.length > 0 ? note.mentions : undefined, mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri ?? undefined, uri: note.uri ?? undefined,

View File

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

View File

@ -77,8 +77,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);
this.writeCloudLogging(level, log, timestamp, level === 'error' || level === 'warning' ? data : null); this.writeCloudLogging(level, log, timestamp, level === 'error' || level === 'warning' ? data : null);
} }

View File

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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()));
}

View File

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

View File

@ -191,6 +191,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,
}) })
@ -565,6 +588,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,
}) })

View File

@ -174,6 +174,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: {

View File

@ -24,6 +24,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';
@ -54,6 +55,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeleteDriveFilesProcessorService, DeleteDriveFilesProcessorService,
ExportCustomEmojisProcessorService, ExportCustomEmojisProcessorService,
ExportNotesProcessorService, ExportNotesProcessorService,
ExportClipsProcessorService,
ExportFavoritesProcessorService, ExportFavoritesProcessorService,
ExportFollowingProcessorService, ExportFollowingProcessorService,
ExportMutingProcessorService, ExportMutingProcessorService,

View File

@ -17,6 +17,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.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';
@ -96,6 +97,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
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,
@ -170,6 +172,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job); case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.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);

View File

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

View File

@ -0,0 +1,206 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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,
},
},
};
}
}

View File

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

View File

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

View File

@ -16,7 +16,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 */

View File

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

View File

@ -216,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';
@ -608,6 +609,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 };
@ -1005,6 +1007,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$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,
@ -1395,6 +1398,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$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,

View File

@ -65,6 +65,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,
@ -82,6 +83,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);

View File

@ -216,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';
@ -606,6 +607,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],

View File

@ -41,6 +41,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,
@ -167,6 +179,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,
@ -336,6 +352,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,
@ -533,6 +561,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
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,
@ -567,6 +598,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
sensitiveWords: instance.sensitiveWords, sensitiveWords: instance.sensitiveWords,
preservedUsernames: instance.preservedUsernames, preservedUsernames: instance.preservedUsernames,
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,
@ -619,6 +651,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,

View File

@ -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 },
@ -137,6 +141,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' },
@ -293,6 +300,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;
} }
@ -577,6 +600,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;
} }

View File

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

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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);
});
}
}

View File

@ -112,6 +112,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,
@ -356,6 +368,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
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,

View File

@ -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がブロックされているユーザーが関わるものだったら無視する

View File

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

View 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.'
}
]
},
};

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

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

View 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"
]
}

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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,

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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;
// アンテナを作成できる最小のパラメタ // アンテナを作成できる最小のパラメタ
@ -55,8 +51,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;
@ -80,10 +74,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' });
@ -137,10 +127,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]) {

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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;

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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', {

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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,

View File

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

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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',

View File

@ -0,0 +1,193 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick 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 'cherrypick-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');
});
});

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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に該当するページはありませんでした。"

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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',

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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', () => {

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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,

View File

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

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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',

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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.CHERRYPICK_TEST_CHECK_IP_RANGE = ''; await sendEnvUpdateRequest({ key: 'CHERRYPICK_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.CHERRYPICK_TEST_CHECK_IP_RANGE = '1'; await sendEnvUpdateRequest({ key: 'CHERRYPICK_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({

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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,

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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(

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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' });

View File

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

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/develop/packages/backend/test/resources/Lenna.png'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/kokonect-link/cherrypick/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,

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-js';
import type { INestApplicationContext } from '@nestjs/common';
describe('ユーザー', () => { describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する // エンティティとしてのユーザーを主眼においたテストを記述する
@ -186,8 +174,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;
@ -231,10 +217,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' });
@ -322,10 +304,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,

View File

@ -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 'cherrypick-js'; import type * as misskey from 'cherrypick-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);

View File

@ -0,0 +1,8 @@
import { initTestDb, sendEnvResetRequest } from './utils.js';
beforeAll(async () => {
await Promise.all([
initTestDb(false),
sendEnvResetRequest(),
]);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 '@/postgres.js';
import { loadConfig } from '@/config.js'; import { loadConfig } from '@/config.js';
import type * as misskey from 'cherrypick-js'; import type * as misskey from 'cherrypick-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.');
}
}

View File

@ -1086,6 +1086,18 @@ export type Endpoints = Overwrite<Endpoints_2, {
}; };
}; };
}; };
'signup': {
req: SignupRequest;
res: SignupResponse;
};
'signup-pending': {
req: SignupPendingRequest;
res: SignupPendingResponse;
};
'signin': {
req: SigninRequest;
res: SigninResponse;
};
}>; }>;
// @public (undocumented) // @public (undocumented)
@ -1105,6 +1117,12 @@ declare namespace entities {
EmojiUpdated, EmojiUpdated,
EmojiDeleted, EmojiDeleted,
AnnouncementCreated, AnnouncementCreated,
SignupRequest,
SignupResponse,
SignupPendingRequest,
SignupPendingResponse,
SigninRequest,
SigninResponse,
EmptyRequest, EmptyRequest,
EmptyResponse, EmptyResponse,
AdminMetaResponse, AdminMetaResponse,
@ -2682,7 +2700,7 @@ type QueueStats = {
}; };
// @public (undocumented) // @public (undocumented)
type QueueStatsLog = string[]; type QueueStatsLog = QueueStats[];
// @public (undocumented) // @public (undocumented)
type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json']; type RenoteMuteCreateRequest = operations['renote-mute/create']['requestBody']['content']['application/json'];
@ -2756,11 +2774,52 @@ type ServerStats = {
}; };
// @public (undocumented) // @public (undocumented)
type ServerStatsLog = string[]; type ServerStatsLog = ServerStats[];
// @public (undocumented) // @public (undocumented)
type Signin = components['schemas']['Signin']; type Signin = components['schemas']['Signin'];
// @public (undocumented)
type SigninRequest = {
username: string;
password: string;
token?: string;
};
// @public (undocumented)
type SigninResponse = {
id: User['id'];
i: string;
};
// @public (undocumented)
type SignupPendingRequest = {
code: string;
};
// @public (undocumented)
type SignupPendingResponse = {
id: User['id'];
i: string;
};
// @public (undocumented)
type SignupRequest = {
username: string;
password: string;
host?: string;
invitationCode?: string;
emailAddress?: string;
'hcaptcha-response'?: string | null;
'g-recaptcha-response'?: string | null;
'turnstile-response'?: string | null;
};
// @public (undocumented)
type SignupResponse = MeDetailed & {
token: string;
};
// @public (undocumented) // @public (undocumented)
type StatsResponse = operations['stats']['responses']['200']['content']['application/json']; type StatsResponse = operations['stats']['responses']['200']['content']['application/json'];

View File

@ -8,15 +8,16 @@
}, },
"devDependencies": { "devDependencies": {
"@apidevtools/swagger-parser": "10.1.0", "@apidevtools/swagger-parser": "10.1.0",
"@misskey-dev/eslint-plugin": "^1.0.0",
"@types/node": "20.9.1", "@types/node": "20.9.1",
"@typescript-eslint/eslint-plugin": "6.11.0", "@typescript-eslint/eslint-plugin": "6.11.0",
"@typescript-eslint/parser": "6.11.0", "@typescript-eslint/parser": "6.11.0",
"eslint": "8.53.0", "eslint": "8.53.0",
"typescript": "5.3.3",
"tsx": "4.4.0",
"ts-case-convert": "2.0.2",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",
"openapi-typescript": "6.7.1" "openapi-typescript": "6.7.1",
"ts-case-convert": "2.0.2",
"tsx": "4.4.0",
"typescript": "5.3.3"
}, },
"files": [ "files": [
"built" "built"

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