Merge branch 'develop' of misskey-dev into merge-upstream

This commit is contained in:
まっちゃとーにゅ 2024-01-10 03:10:45 +09:00
commit abe95d9b67
No known key found for this signature in database
GPG Key ID: 143DE582A97FE052
376 changed files with 5293 additions and 2636 deletions

View File

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

View File

@ -17,16 +17,4 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
# PNPM has an issue with dependabot. See:
# https://github.com/dependabot/dependabot-core/issues/7258
# https://github.com/pnpm/pnpm/issues/6530
# TODO: Restore this when the issue is solved
open-pull-requests-limit: 0 open-pull-requests-limit: 0
groups:
swc:
patterns:
- "@swc/*"
storybook:
patterns:
- "storybook*"
- "@storybook/*"

View File

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

View File

@ -3,9 +3,20 @@ name: Lint
on: on:
push: push:
branches: branches:
- master - io
- develop paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
pull_request: pull_request:
paths:
- packages/backend/**
- packages/frontend/**
- packages/sw/**
- packages/misskey-js/**
- packages/shared/.eslintrc.js
jobs: jobs:
pnpm_install: pnpm_install:
@ -44,7 +55,7 @@ jobs:
submodules: true submodules: true
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 7 version: 8
run_install: false run_install: false
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.1
with: with:
@ -70,7 +81,7 @@ jobs:
submodules: true submodules: true
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 7 version: 8
run_install: false run_install: false
- uses: actions/setup-node@v4.0.1 - uses: actions/setup-node@v4.0.1
with: with:

View File

@ -3,12 +3,19 @@ name: Test (backend)
on: on:
push: push:
branches: branches:
- master - io
- develop paths:
- packages/backend/**
# for permissions
- packages/misskey-js/**
pull_request: pull_request:
paths:
- packages/backend/**
# for permissions
- packages/misskey-js/**
jobs: jobs:
jest: unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@ -51,8 +58,58 @@ 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
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
services:
postgres:
image: postgres:15
ports:
- 54312:5432
env:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
keydb:
image: eqalpha/keydb:latest
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/misskey/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 uses: codecov/codecov-action@v3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -3,9 +3,21 @@ name: Test (frontend)
on: on:
push: push:
branches: branches:
- master - io
- develop paths:
- packages/frontend/**
# for permissions
- packages/misskey-js/**
# for e2e
- packages/backend/**
pull_request: pull_request:
paths:
- packages/frontend/**
# for permissions
- packages/misskey-js/**
# for e2e
- packages/backend/**
jobs: jobs:
vitest: vitest:
@ -80,7 +92,7 @@ jobs:
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
with: with:
version: 7 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.1

View File

@ -5,9 +5,12 @@ name: Test (misskey.js)
on: on:
push: push:
branches: [ develop ] branches: [ io ]
paths:
- packages/misskey-js/**
pull_request: pull_request:
branches: [ develop ] paths:
- packages/misskey-js/**
jobs: jobs:
test: test:

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

@ -0,0 +1,46 @@
name: Test (backend)
on:
push:
branches:
- io
paths:
- packages/backend/**
pull_request:
paths:
- packages/backend/**
jobs:
validate-api-json:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
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

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

View File

@ -12,11 +12,36 @@
--> -->
## 202x.x.x (Unreleased)
### General
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
### Client
- Feat: 新しいゲームを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
- Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
### 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 +149,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

@ -1,5 +1,5 @@
Unless otherwise stated this repository is Unless otherwise stated this repository is
Copyright © 2014-2023 syuilo and contributers Copyright © 2014-2024 syuilo and contributors
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.

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
- keydb - keydb
# - 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

22
locales/index.d.ts vendored
View File

@ -386,6 +386,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;
@ -633,6 +638,7 @@ export interface Locale {
"small": string; "small": string;
"generateAccessToken": string; "generateAccessToken": string;
"permission": string; "permission": string;
"adminPermission": string;
"enableAll": string; "enableAll": string;
"disableAll": string; "disableAll": string;
"tokenRequested": string; "tokenRequested": string;
@ -675,6 +681,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;
@ -1194,6 +1201,11 @@ export interface Locale {
"addMfmFunction": string; "addMfmFunction": string;
"enableQuickAddMfmFunction": string; "enableQuickAddMfmFunction": string;
"bubbleGame": string; "bubbleGame": string;
"sfx": string;
"soundWillBePlayed": string;
"showReplay": string;
"replay": string;
"replaying": string;
"abuseReportCategory": string; "abuseReportCategory": string;
"selectCategory": string; "selectCategory": string;
"reportComplete": string; "reportComplete": string;
@ -1680,6 +1692,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": {
@ -2288,6 +2309,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

@ -383,6 +383,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: "サイトキー"
@ -630,6 +635,7 @@ medium: "中"
small: "小" small: "小"
generateAccessToken: "アクセストークンの発行" generateAccessToken: "アクセストークンの発行"
permission: "権限" permission: "権限"
adminPermission: "管理者権限"
enableAll: "全て有効にする" enableAll: "全て有効にする"
disableAll: "全て無効にする" disableAll: "全て無効にする"
tokenRequested: "アカウントへのアクセス許可" tokenRequested: "アカウントへのアクセス許可"
@ -672,6 +678,7 @@ useGlobalSettingDesc: "オンにすると、アカウントの通知設定が使
other: "その他" other: "その他"
regenerateLoginToken: "ログイントークンを再生成" regenerateLoginToken: "ログイントークンを再生成"
regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。" regenerateLoginTokenDescription: "ログインに使用される内部トークンを再生成します。通常この操作を行う必要はありません。再生成すると、全てのデバイスでログアウトされます。"
theKeywordWhenSearchingForCustomEmoji: "カスタム絵文字を検索する時のキーワードになります。"
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL" fileIdOrUrl: "ファイルIDまたはURL"
behavior: "動作" behavior: "動作"
@ -1191,6 +1198,11 @@ decorate: "デコる"
addMfmFunction: "装飾を追加" addMfmFunction: "装飾を追加"
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
bubbleGame: "バブルゲーム" bubbleGame: "バブルゲーム"
sfx: "効果音"
soundWillBePlayed: "サウンドが再生されます"
showReplay: "リプレイを見る"
replay: "リプレイ"
replaying: "リプレイ中"
abuseReportCategory: "通報の種類" abuseReportCategory: "通報の種類"
selectCategory: "カテゴリを選択" selectCategory: "カテゴリを選択"
reportComplete: "通報完了" reportComplete: "通報完了"
@ -1591,6 +1603,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "Misskey初心者講座 修了証" title: "Misskey初心者講座 修了証"
description: "チュートリアルを完了した" description: "チュートリアルを完了した"
_bubbleGameExplodingHead:
title: "🤯"
description: "バブルゲームで最も大きいモノを出した"
_bubbleGameDoubleExplodingHead:
title: "ダブル🤯"
description: "バブルゲームで最も大きいモを2つ同時に出した"
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
@ -2191,6 +2210,7 @@ _profile:
_exportOrImport: _exportOrImport:
allNotes: "全てのノート" allNotes: "全てのノート"
favoritedNotes: "お気に入りにしたノート" favoritedNotes: "お気に入りにしたノート"
clips: "クリップ"
followingList: "フォロー" followingList: "フォロー"
muteList: "ミュート" muteList: "ミュート"
blockingList: "ブロック" blockingList: "ブロック"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.12.2-io.2b", "version": "2023.12.2-io.2c",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
@ -18,7 +18,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-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build", "build-misskey-js-with-types": "pnpm --filter backend build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
"start": "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 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 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"
}, },
"optionalDependencies": { "optionalDependencies": {
@ -74,6 +79,8 @@
"@fastify/multipart": "8.0.0", "@fastify/multipart": "8.0.0",
"@fastify/static": "6.12.0", "@fastify/static": "6.12.0",
"@fastify/view": "8.2.0", "@fastify/view": "8.2.0",
"@misskey-dev/sharp-read-bmp": "^1.1.1",
"@misskey-dev/summaly": "^5.0.3",
"@nestjs/common": "10.2.10", "@nestjs/common": "10.2.10",
"@nestjs/core": "10.2.10", "@nestjs/core": "10.2.10",
"@nestjs/testing": "10.2.10", "@nestjs/testing": "10.2.10",
@ -157,11 +164,9 @@
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.32.6", "sharp": "0.32.6",
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "github:misskey-dev/summaly",
"systeminformation": "5.21.20", "systeminformation": "5.21.20",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.1", "tmp": "0.2.1",
@ -177,6 +182,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",
@ -225,9 +232,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';
@ -13,6 +12,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 = {
@ -34,7 +34,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 {
@ -128,17 +128,12 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
) {} ) { }
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') { // Wait for all potential DB queries
// XXX: await allSettled();
// Shutting down the existing connections causes errors on Jest as // And then disconnect from DB
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
await Promise.all([ await Promise.all([
this.db.destroy(), this.db.destroy(),
this.redisClient.disconnect(), this.redisClient.disconnect(),

View File

@ -87,6 +87,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';
@ -655,7 +655,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

@ -156,7 +156,7 @@ export class EmailService {
@bindThis @bindThis
public async validateEmailForAccount(emailAddress: string): Promise<{ public async validateEmailForAccount(emailAddress: string): Promise<{
available: boolean; available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned'; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist';
}> { }> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
@ -183,6 +183,10 @@ export class EmailService {
if (validated.valid && meta.enableVerifymailApi && meta.verifymailAuthKey != null) { if (validated.valid && meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
} }
if (validated.valid && meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
}
} else { } else {
validated = { valid: true, reason: null }; validated = { valid: true, reason: null };
} }
@ -201,6 +205,8 @@ export class EmailService {
validated.reason === 'disposable' ? 'disposable' : validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' : validated.reason === 'mx' ? 'mx' :
validated.reason === 'smtp' ? 'smtp' : validated.reason === 'smtp' ? 'smtp' :
validated.reason === 'network' ? 'network' :
validated.reason === 'blacklist' ? 'blacklist' :
null, null,
}; };
} }
@ -265,4 +271,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

@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -677,7 +678,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,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { setTimeout } from 'node:timers/promises';
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
import * as Bull from 'bullmq'; import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { QUEUE, baseQueueOptions } from '@/queue/const.js'; import { QUEUE, baseQueueOptions } from '@/queue/const.js';
import { allSettled } from '@/misc/promise-tracker.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js'; import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
) {} ) {}
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
if (process.env.NODE_ENV === 'test') { // Wait for all potential queue jobs
// XXX: await allSettled();
// Shutting down the existing connections causes errors on Jest as // And then close all queues
// Misskey has asynchronous postgres/redis connections that are not
// awaited.
// Let's wait for some random time for them to finish.
await setTimeout(5000);
}
await Promise.all([ await Promise.all([
this.systemQueue.close(), this.systemQueue.close(),
this.endedPollNotificationQueue.close(), this.endedPollNotificationQueue.close(),

View File

@ -183,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;
@ -273,7 +274,7 @@ export class ReactionService {
} }
} }
dm.execute(); trackPromise(dm.execute());
} }
//#endregion //#endregion
} }
@ -321,7 +322,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

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

@ -84,8 +84,11 @@ export default class Logger {
let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`; let log = `${l} ${worker}\t[${contexts.join(' ')}]\t${m}`;
if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log; if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log); const args: unknown[] = [important ? chalk.bold(log) : log];
if (level === 'error' && data) console.log(data); if (data != null) {
args.push(data);
}
console.log(...args);
} }
@bindThis @bindThis

View File

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
/**
* This tracks promises that other modules decided not to wait for,
* and makes sure they are all settled before fully closing down the server.
*/
export function trackPromise(promise: Promise<unknown>) {
if (process.env.NODE_ENV !== 'test') {
return;
}
const ref = new WeakRef(promise);
promiseRefs.add(ref);
promise.finally(() => promiseRefs.delete(ref));
}
export async function allSettled(): Promise<void> {
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
}

View File

@ -196,6 +196,29 @@ export class MiMeta {
}) })
public hcaptchaSecretKey: string | null; public hcaptchaSecretKey: string | null;
@Column('boolean', {
default: false,
})
public enableMcaptcha: boolean;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaSitekey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaSecretKey: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public mcaptchaInstanceUrl: string | null;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })
@ -462,6 +485,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

@ -148,6 +148,10 @@ export const packedNoteSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
userId: {
type: 'string',
optional: false, nullable: true,
},
}, },
}, },
localOnly: { localOnly: {

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

@ -16,6 +16,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';
@ -92,6 +93,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,
@ -166,6 +168,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

@ -0,0 +1,206 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as fs from 'node:fs';
import { Writable } from 'node:stream';
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { format as dateFormat } from 'date-fns';
import { DI } from '@/di-symbols.js';
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@Injectable()
export class ExportClipsProcessorService {
private logger: Logger;
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
@Inject(DI.clipNotesRepository)
private clipNotesRepository: ClipNotesRepository,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
}
@bindThis
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
if (user == null) {
return;
}
// Create temp file
const [path, cleanup] = await createTemp();
this.logger.info(`Temp file is ${path}`);
try {
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
const writer = stream.getWriter();
writer.closed.catch(this.logger.error);
await writer.write('[');
await this.processClips(writer, user, job);
await writer.write(']');
await writer.close();
this.logger.succ(`Exported to: ${path}`);
const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
} finally {
cleanup();
}
}
async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
let exportedClipsCount = 0;
let cursor: MiClip['id'] | null = null;
while (true) {
const clips = await this.clipsRepository.find({
where: {
userId: user.id,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
});
if (clips.length === 0) {
job.updateProgress(100);
break;
}
cursor = clips.at(-1)?.id ?? null;
for (const clip of clips) {
// Stringify but remove the last `]}`
const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
const isFirst = exportedClipsCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
await this.processClipNotes(writer, clip.id);
await writer.write(']}');
exportedClipsCount++;
}
const total = await this.clipsRepository.countBy({
userId: user.id,
});
job.updateProgress(exportedClipsCount / total);
}
}
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
let exportedClipNotesCount = 0;
let cursor: MiClipNote['id'] | null = null;
while (true) {
const clipNotes = await this.clipNotesRepository.find({
where: {
clipId,
...(cursor ? { id: MoreThan(cursor) } : {}),
},
take: 100,
order: {
id: 1,
},
relations: ['note', 'note.user'],
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
if (clipNotes.length === 0) {
break;
}
cursor = clipNotes.at(-1)?.id ?? null;
for (const clipNote of clipNotes) {
let poll: MiPoll | undefined;
if (clipNote.note.hasPoll) {
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
}
const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
const isFirst = exportedClipNotesCount === 0;
await writer.write(isFirst ? content : ',\n' + content);
exportedClipNotesCount++;
}
}
}
private serializeClip(clip: MiClip): Record<string, unknown> {
return {
id: clip.id,
name: clip.name,
description: clip.description,
lastClippedAt: clip.lastClippedAt?.toISOString(),
clipNotes: [],
};
}
private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
return {
id: clip.id,
createdAt: this.idService.parse(clip.id).date.toISOString(),
note: {
id: clip.note.id,
text: clip.note.text,
createdAt: this.idService.parse(clip.note.id).date.toISOString(),
fileIds: clip.note.fileIds,
replyId: clip.note.replyId,
renoteId: clip.note.renoteId,
poll: poll,
cw: clip.note.cw,
visibility: clip.note.visibility,
visibleUserIds: clip.note.visibleUserIds,
localOnly: clip.note.localOnly,
reactionAcceptance: clip.note.reactionAcceptance,
uri: clip.note.uri,
url: clip.note.url,
user: {
id: clip.note.user.id,
name: clip.note.user.name,
username: clip.note.user.username,
host: clip.note.user.host,
uri: clip.note.user.uri,
},
},
};
}
}

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

@ -212,6 +212,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';
@ -578,6 +579,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 };
@ -948,6 +950,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,
@ -1312,6 +1315,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

@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { Schema } from '@/misc/json-schema.js';
import { permissions } from 'misskey-js'; import { permissions } from 'misskey-js';
import type { Schema } from '@/misc/json-schema.js';
import { RolePolicies } from '@/core/RoleService.js'; import { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_meta from './endpoints/admin/meta.js';
@ -213,6 +213,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';
@ -577,6 +578,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,
@ -173,6 +185,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,
@ -294,6 +310,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,
@ -474,6 +502,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,
@ -505,6 +536,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,
@ -539,6 +571,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

@ -63,6 +63,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 },
@ -116,6 +120,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' },
@ -289,6 +296,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;
} }
@ -493,6 +516,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 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

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

@ -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 RE2 from 're2'; import RE2 from 're2';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('2要素認証', () => { describe('2要素認証', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup;
const config = loadConfig(); const config = loadConfig();
const password = 'test'; const password = 'test';
@ -185,14 +183,9 @@ describe('2要素認証', () => {
}; };
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username, password }); alice = await signup({ username, password });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('が設定でき、OTPでログインできる。', async () => { test('が設定でき、OTPでログインできる。', async () => {
const registerResponse = await api('/i/2fa/register', { const registerResponse = await api('/i/2fa/register', {
password, password,

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 'misskey-js'; import type * as misskey from 'misskey-js';
import type { INestApplicationContext } from '@nestjs/common';
const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => { const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
return selector(a).localeCompare(selector(b)); return selector(a).localeCompare(selector(b));
@ -37,7 +33,7 @@ describe('アンテナ', () => {
// - srcのenumにgroupが残っている // - srcのenumにgroupが残っている
// - userGroupIdが残っている, isActiveがない // - userGroupIdが残っている, isActiveがない
type Antenna = misskey.entities.Antenna | Packed<'Antenna'>; type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
type User = misskey.entities.MeSignup; type User = misskey.entities.SignupResponse;
type Note = misskey.entities.Note; type Note = misskey.entities.Note;
// アンテナを作成できる最小のパラメタ // アンテナを作成できる最小のパラメタ
@ -54,8 +50,6 @@ describe('アンテナ', () => {
withReplies: false, withReplies: false,
}; };
let app: INestApplicationContext;
let root: User; let root: User;
let alice: User; let alice: User;
let bob: User; let bob: User;
@ -79,10 +73,6 @@ describe('アンテナ', () => {
let userMutingAlice: User; let userMutingAlice: User;
let userMutedByAlice: User; let userMutedByAlice: User;
beforeAll(async () => {
app = await startServer();
}, 1000 * 60 * 2);
beforeAll(async () => { beforeAll(async () => {
root = await signup({ username: 'root' }); root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
@ -136,10 +126,6 @@ describe('アンテナ', () => {
await api('mute/create', { userId: userMutedByAlice.id }, alice); await api('mute/create', { userId: userMutedByAlice.id }, alice);
}, 1000 * 60 * 10); }, 1000 * 60 * 10);
afterAll(async () => {
await app.close();
});
beforeEach(async () => { beforeEach(async () => {
// テスト間で影響し合わないように毎回全部消す。 // テスト間で影響し合わないように毎回全部消す。
for (const user of [alice, bob]) { for (const user of [alice, bob]) {

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('API visibility', () => { describe('API visibility', () => {
let app: INestApplicationContext;
beforeAll(async () => {
app = await startServer();
}, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('Note visibility', () => { describe('Note visibility', () => {
//#region vars //#region vars
/** ヒロイン */ /** ヒロイン */
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
/** フォロワー */ /** フォロワー */
let follower: misskey.entities.MeSignup; let follower: misskey.entities.SignupResponse;
/** 非フォロワー */ /** 非フォロワー */
let other: misskey.entities.MeSignup; let other: misskey.entities.SignupResponse;
/** 非フォロワーでもリプライやメンションをされた人 */ /** 非フォロワーでもリプライやメンションをされた人 */
let target: misskey.entities.MeSignup; let target: misskey.entities.SignupResponse;
/** specified mentionでmentionを飛ばされる人 */ /** specified mentionでmentionを飛ばされる人 */
let target2: misskey.entities.MeSignup; let target2: misskey.entities.SignupResponse;
/** public-post */ /** public-post */
let pub: any; let pub: any;

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('API', () => { describe('API', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('General validation', () => { describe('General validation', () => {
test('wrong type', async () => { test('wrong type', async () => {
const res = await api('/test', { const res = await api('/test', {

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Block', () => { describe('Block', () => {
let app: INestApplicationContext;
// alice blocks bob // alice blocks bob
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('Block作成', async () => { test('Block作成', async () => {
const res = await api('/blocking/create', { const res = await api('/blocking/create', {
userId: bob.id, userId: bob.id,

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Endpoints', () => { describe('Endpoints', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let dave: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup;
let dave: misskey.entities.MeSignup;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' }); dave = await signup({ username: 'dave' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('signup', () => { describe('signup', () => {
test('不正なユーザー名でアカウントが作成できない', async () => { test('不正なユーザー名でアカウントが作成できない', async () => {
const res = await api('signup', { const res = await api('signup', {
@ -710,6 +702,18 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400); assert.strictEqual(res.status, 400);
}); });
test('不正なファイル名で怒られる', async () => {
const file = (await uploadFile(alice)).body;
const newName = '';
const res = await api('/drive/files/update', {
fileId: file.id,
name: newName,
}, alice);
assert.strictEqual(res.status, 400);
});
test('間違ったIDで怒られる', async () => { test('間違ったIDで怒られる', async () => {
const res = await api('/drive/files/update', { const res = await api('/drive/files/update', {
fileId: 'kyoppie', fileId: 'kyoppie',

View File

@ -0,0 +1,193 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { api, port, post, signup, startJobQueue } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
describe('export-clips', () => {
let queue: INestApplicationContext;
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
// XXX: Any better way to get the result?
async function pollFirstDriveFile() {
while (true) {
const files = (await api('/drive/files', {}, alice)).body;
if (!files.length) {
await new Promise(r => setTimeout(r, 100));
continue;
}
if (files.length > 1) {
throw new Error('Too many files?');
}
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
return await res.json();
}
}
beforeAll(async () => {
queue = await startJobQueue();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
afterAll(async () => {
await queue.close();
});
beforeEach(async () => {
// Clean all clips and files of alice
const clips = (await api('/clips/list', {}, alice)).body;
for (const clip of clips) {
const res = await api('/clips/delete', { clipId: clip.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete clip');
}
}
const files = (await api('/drive/files', {}, alice)).body;
for (const file of files) {
const res = await api('/drive/files/delete', { fileId: file.id }, alice);
if (res.status !== 204) {
throw new Error('Failed to delete file');
}
}
});
test('basic export', async () => {
let res = await api('/clips/create', {
name: 'foo',
description: 'bar',
}, alice);
assert.strictEqual(res.status, 200);
res = await api('/i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'foo');
assert.strictEqual(exported[0].description, 'bar');
assert.strictEqual(exported[0].clipNotes.length, 0);
});
test('export with notes', async () => {
let res = await api('/clips/create', {
name: 'foo',
description: 'bar',
}, alice);
assert.strictEqual(res.status, 200);
const clip = res.body;
const note1 = await post(alice, {
text: 'baz1',
});
const note2 = await post(alice, {
text: 'baz2',
poll: {
choices: ['sakura', 'izumi', 'ako'],
},
});
for (const note of [note1, note2]) {
res = await api('/clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
}
res = await api('/i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'foo');
assert.strictEqual(exported[0].description, 'bar');
assert.strictEqual(exported[0].clipNotes.length, 2);
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2');
assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura');
});
test('multiple clips', async () => {
let res = await api('/clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
assert.strictEqual(res.status, 200);
const clip1 = res.body;
res = await api('/clips/create', {
name: 'yuri',
description: 'yuri',
}, alice);
assert.strictEqual(res.status, 200);
const clip2 = res.body;
const note1 = await post(alice, {
text: 'baz1',
});
const note2 = await post(alice, {
text: 'baz2',
});
res = await api('/clips/add-note', {
clipId: clip1.id,
noteId: note1.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/clips/add-note', {
clipId: clip2.id,
noteId: note2.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'kawaii');
assert.strictEqual(exported[0].clipNotes.length, 1);
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
assert.strictEqual(exported[1].name, 'yuri');
assert.strictEqual(exported[1].clipNotes.length, 1);
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
});
test('Clipping other user\'s note', async () => {
let res = await api('/clips/create', {
name: 'kawaii',
description: 'kawaii',
}, alice);
assert.strictEqual(res.status, 200);
const clip = res.body;
const note = await post(bob, {
text: 'baz',
visibility: 'followers',
});
res = await api('/clips/add-note', {
clipId: clip.id,
noteId: note.id,
}, alice);
assert.strictEqual(res.status, 204);
res = await api('/i/export-clips', {}, alice);
assert.strictEqual(res.status, 204);
const exported = await pollFirstDriveFile();
assert.strictEqual(exported[0].name, 'kawaii');
assert.strictEqual(exported[0].clipNotes.length, 1);
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz');
assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob');
});
});

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 'misskey-js'; import type * as misskey from 'misskey-js';
// Request Accept // Request Accept
@ -23,9 +22,7 @@ const HTML = 'text/html; charset=utf-8';
const JSON_UTF8 = 'application/json; charset=utf-8'; const JSON_UTF8 = 'application/json; charset=utf-8';
describe('Webリソース', () => { describe('Webリソース', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup;
let aliceUploadedFile: any; let aliceUploadedFile: any;
let alicesPost: any; let alicesPost: any;
let alicePage: any; let alicePage: any;
@ -34,7 +31,7 @@ describe('Webリソース', () => {
let aliceGalleryPost: any; let aliceGalleryPost: any;
let aliceChannel: any; let aliceChannel: any;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
type Request = { type Request = {
path: string, path: string,
@ -79,7 +76,6 @@ describe('Webリソース', () => {
}; };
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
aliceUploadedFile = await uploadFile(alice); aliceUploadedFile = await uploadFile(alice);
alicesPost = await post(alice, { alicesPost = await post(alice, {
@ -96,10 +92,6 @@ describe('Webリソース', () => {
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe.each([ describe.each([
{ path: '/', type: HTML }, { path: '/', type: HTML },
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。" { path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('FF visibility', () => { describe('FF visibility', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => { test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
await api('/i/update', { await api('/i/update', {
followingVisibility: 'public', followingVisibility: 'public',

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Account Move', () => { describe('Account Move', () => {
let app: INestApplicationContext;
let jq: INestApplicationContext; let jq: INestApplicationContext;
let url: URL; let url: URL;
let root: any; let root: any;
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
let dave: misskey.entities.MeSignup; let dave: misskey.entities.SignupResponse;
let eve: misskey.entities.MeSignup; let eve: misskey.entities.SignupResponse;
let frank: misskey.entities.MeSignup; let frank: misskey.entities.SignupResponse;
let Users: UsersRepository; let Users: UsersRepository;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
jq = await jobQueue(); jq = await jobQueue();
const config = loadConfig(); const config = loadConfig();
url = new URL(config.url); url = new URL(config.url);
const connection = await initTestDb(false); const connection = await initTestDb(false);
@ -46,7 +46,7 @@ describe('Account Move', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => { afterAll(async () => {
await Promise.all([app.close(), jq.close()]); await jq.close();
}); });
describe('Create Alias', () => { describe('Create Alias', () => {

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Mute', () => { describe('Mute', () => {
let app: INestApplicationContext;
// alice mutes carol // alice mutes carol
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/mute/create', { const res = await api('/mute/create', {
userId: carol.id, userId: carol.id,

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Note', () => { describe('Note', () => {
let app: INestApplicationContext;
let Notes: any; let Notes: any;
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
const connection = await initTestDb(true); const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote); Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('投稿できる', async () => { test('投稿できる', async () => {
const post = { const post = {
text: 'test', text: 'test',

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 'misskey-js'; import type * as misskey from 'misskey-js';
import type { INestApplicationContext } from '@nestjs/common';
const host = `http://127.0.0.1:${port}`; const host = `http://127.0.0.1:${port}`;
@ -75,7 +80,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName:
}; };
} }
function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> { function fetchDecision(transactionId: string, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
return fetch(new URL('/oauth/decision', host), { return fetch(new URL('/oauth/decision', host), {
method: 'post', method: 'post',
body: new URLSearchParams({ body: new URLSearchParams({
@ -90,14 +95,14 @@ function fetchDecision(transactionId: string, user: misskey.entities.MeSignup, {
}); });
} }
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> { async function fetchDecisionFromResponse(response: Response, user: misskey.entities.SignupResponse, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
const { transactionId } = getMeta(await response.text()); const { transactionId } = getMeta(await response.text());
assert.ok(transactionId); assert.ok(transactionId);
return await fetchDecision(transactionId, user, { cancel }); return await fetchDecision(transactionId, user, { cancel });
} }
async function fetchAuthorizationCode(user: misskey.entities.MeSignup, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> { async function fetchAuthorizationCode(user: misskey.entities.SignupResponse, scope: string, code_challenge: string): Promise<{ client: AuthorizationCode, code: string }> {
const client = new AuthorizationCode(clientConfig); const client = new AuthorizationCode(clientConfig);
const response = await fetch(client.authorizeURL({ const response = await fetch(client.authorizeURL({
@ -147,16 +152,14 @@ async function assertDirectError(response: Response, status: number, error: stri
} }
describe('OAuth', () => { describe('OAuth', () => {
let app: INestApplicationContext;
let fastify: FastifyInstance; let fastify: FastifyInstance;
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let sender: (reply: FastifyReply) => void; let sender: (reply: FastifyReply) => void;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
@ -168,7 +171,7 @@ describe('OAuth', () => {
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
beforeEach(async () => { beforeEach(async () => {
process.env.MISSKEY_TEST_CHECK_IP_RANGE = ''; await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' });
sender = (reply): void => { sender = (reply): void => {
reply.send(` reply.send(`
<!DOCTYPE html> <!DOCTYPE html>
@ -180,7 +183,6 @@ describe('OAuth', () => {
afterAll(async () => { afterAll(async () => {
await fastify.close(); await fastify.close();
await app.close();
}); });
test('Full flow', async () => { test('Full flow', async () => {
@ -881,7 +883,7 @@ describe('OAuth', () => {
}); });
test('Disallow loopback', async () => { test('Disallow loopback', async () => {
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1'; await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' });
const client = new AuthorizationCode(clientConfig); const client = new AuthorizationCode(clientConfig);
const response = await fetch(client.authorizeURL({ const response = await fetch(client.authorizeURL({

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Renote Mute', () => { describe('Renote Mute', () => {
let app: INestApplicationContext;
// alice mutes carol // alice mutes carol
let alice: misskey.entities.MeSignup; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('ミュート作成', async () => { test('ミュート作成', async () => {
const res = await api('/renote-mute/create', { const res = await api('/renote-mute/create', {
userId: carol.id, userId: carol.id,

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Streaming', () => { describe('Streaming', () => {
let app: INestApplicationContext;
let Followings: any; let Followings: any;
const follow = async (follower: any, followee: any) => { const follow = async (follower: any, followee: any) => {
@ -32,15 +30,15 @@ describe('Streaming', () => {
describe('Streaming', () => { describe('Streaming', () => {
// Local users // Local users
let ayano: misskey.entities.MeSignup; let ayano: misskey.entities.SignupResponse;
let kyoko: misskey.entities.MeSignup; let kyoko: misskey.entities.SignupResponse;
let chitose: misskey.entities.MeSignup; let chitose: misskey.entities.SignupResponse;
let kanako: misskey.entities.MeSignup; let kanako: misskey.entities.SignupResponse;
// Remote users // Remote users
let akari: misskey.entities.MeSignup; let akari: misskey.entities.SignupResponse;
let chinatsu: misskey.entities.MeSignup; let chinatsu: misskey.entities.SignupResponse;
let takumi: misskey.entities.MeSignup; let takumi: misskey.entities.SignupResponse;
let kyokoNote: any; let kyokoNote: any;
let kanakoNote: any; let kanakoNote: any;
@ -48,7 +46,6 @@ describe('Streaming', () => {
let list: any; let list: any;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
const connection = await initTestDb(true); const connection = await initTestDb(true);
Followings = connection.getRepository(MiFollowing); Followings = connection.getRepository(MiFollowing);
@ -95,10 +92,6 @@ describe('Streaming', () => {
}, chitose); }, chitose);
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
describe('Events', () => { describe('Events', () => {
test('mention event', async () => { test('mention event', async () => {
const fired = await waitFire( const fired = await waitFire(

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Note thread mute', () => { describe('Note thread mute', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup; let carol: misskey.entities.SignupResponse;
let bob: misskey.entities.MeSignup;
let carol: misskey.entities.MeSignup;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' }); bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' }); carol = await signup({ username: 'carol' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => { test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
const bobNote = await post(bob, { text: '@alice @carol root note' }); const bobNote = await post(bob, { text: '@alice @carol root note' });
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });

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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('users/notes', () => { describe('users/notes', () => {
let app: INestApplicationContext; let alice: misskey.entities.SignupResponse;
let alice: misskey.entities.MeSignup;
let jpgNote: any; let jpgNote: any;
let pngNote: any; let pngNote: any;
let jpgPngNote: any; let jpgPngNote: any;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/MisskeyIO/misskey/io/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/MisskeyIO/misskey/io/packages/backend/test/resources/Lenna.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/MisskeyIO/misskey/io/packages/backend/test/resources/Lenna.png'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/MisskeyIO/misskey/io/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 'misskey-js'; import type * as misskey from 'misskey-js';
import type { INestApplicationContext } from '@nestjs/common';
describe('ユーザー', () => { describe('ユーザー', () => {
// エンティティとしてのユーザーを主眼においたテストを記述する // エンティティとしてのユーザーを主眼においたテストを記述する
@ -185,8 +173,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;
@ -230,10 +216,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' });
@ -321,10 +303,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 'misskey-js'; import type * as misskey from 'misskey-js';
describe('.well-known', () => { describe('.well-known', () => {
let app: INestApplicationContext;
let alice: misskey.entities.User; let alice: misskey.entities.User;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async () => {
await app.close();
});
test('nodeinfo', async () => { test('nodeinfo', async () => {
const res = await relativeFetch('.well-known/nodeinfo'); const res = await relativeFetch('.well-known/nodeinfo');
assert.ok(res.ok); assert.ok(res.ok);

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

@ -11,7 +11,13 @@ 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 { AnnouncementEntityService } from "@/core/entities/AnnouncementEntityService.js"; import { AnnouncementEntityService } from "@/core/entities/AnnouncementEntityService.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

@ -56,7 +56,8 @@ describe('FetchInstanceMetadataService', () => {
return { fetch: jest.fn() }; return { fetch: jest.fn() };
} else if (token === DI.redis) { } else if (token === DI.redis) {
return mockRedis; return mockRedis;
}}) }
})
.compile(); .compile();
app.enableShutdownHooks(); app.enableShutdownHooks();

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 '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
export { server as startServer } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
interface UserToken { interface UserToken {
token: string; token: string;
@ -68,7 +68,11 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
return res.body; return res.body;
}; };
const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => { const request = async (path: string, params: any, me?: UserToken): Promise<{
status: number,
headers: Headers,
body: any
}> => {
const bodyAuth: Record<string, string> = {}; const bodyAuth: Record<string, string> = {};
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -275,7 +279,11 @@ interface UploadOptions {
* Upload file * Upload file
* @param user User * @param user User
*/ */
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => { export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
status: number,
headers: Headers,
body: misskey.Endpoints['drive/files/create']['res'] | null
}> => {
const absPath = path == null const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url) ? new URL('resources/Lenna.jpg', import.meta.url)
: isAbsolute(path.toString()) : isAbsolute(path.toString())
@ -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.');
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,7 +4,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"watch": "vite", "watch": "vite",
"dev": "vite --config vite.config.local-dev.ts", "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build", "build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"", "storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js", "build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
@ -19,6 +19,8 @@
"dependencies": { "dependencies": {
"@discordapp/twemoji": "15.0.2", "@discordapp/twemoji": "15.0.2",
"@github/webauthn-json": "2.1.1", "@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@rollup/plugin-typescript": "11.1.5", "@rollup/plugin-typescript": "11.1.5",
@ -26,12 +28,11 @@
"@syuilo/aiscript": "0.16.0", "@syuilo/aiscript": "0.16.0",
"@tabler/icons-webfont": "2.44.0", "@tabler/icons-webfont": "2.44.0",
"@twemoji/parser": "15.0.0", "@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "4.5.2", "@vitejs/plugin-vue": "5.0.2",
"@vue/compiler-sfc": "3.3.12", "@vue/compiler-sfc": "3.4.3",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
"astring": "1.8.6", "astring": "1.8.6",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1", "buraha": "0.0.1",
"canvas-confetti": "1.6.1", "canvas-confetti": "1.6.1",
"chart.js": "4.4.1", "chart.js": "4.4.1",
@ -46,7 +47,6 @@
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"eventemitter3": "5.0.1", "eventemitter3": "5.0.1",
"gsap": "3.12.4",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
@ -59,6 +59,7 @@
"rollup": "4.9.1", "rollup": "4.9.1",
"sanitize-html": "2.11.0", "sanitize-html": "2.11.0",
"sass": "1.69.5", "sass": "1.69.5",
"seedrandom": "^3.0.5",
"shiki": "0.14.7", "shiki": "0.14.7",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
@ -71,10 +72,12 @@
"uuid": "9.0.1", "uuid": "9.0.1",
"v-code-diff": "1.7.2", "v-code-diff": "1.7.2",
"vite": "5.0.10", "vite": "5.0.10",
"vue": "3.3.12", "vue": "3.4.3",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "^1.0.0",
"@misskey-dev/summaly": "^5.0.3",
"@storybook/addon-actions": "7.6.5", "@storybook/addon-actions": "7.6.5",
"@storybook/addon-essentials": "7.6.5", "@storybook/addon-essentials": "7.6.5",
"@storybook/addon-interactions": "7.6.5", "@storybook/addon-interactions": "7.6.5",
@ -108,7 +111,7 @@
"@typescript-eslint/eslint-plugin": "6.14.0", "@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.14.0", "@typescript-eslint/parser": "6.14.0",
"@vitest/coverage-v8": "0.34.6", "@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.3.12", "@vue/runtime-core": "3.4.3",
"acorn": "8.11.2", "acorn": "8.11.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.6.1", "cypress": "13.6.1",
@ -128,11 +131,10 @@
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.3",
"storybook": "7.6.5", "storybook": "7.6.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6", "vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.3.2", "vue-eslint-parser": "9.3.2",
"vue-tsc": "1.8.25" "vue-tsc": "1.8.27"
} }
} }

View File

@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js';
import { MenuButton } from '@/types/menu.js'; import { MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js'; import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@/config.js'; import { apiUrl } from '@/config.js';
import { waiting, api, popup, popupMenu, success, alert } from '@/os.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -23,9 +24,14 @@ const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに // TODO: 外部からはreadonlyに
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
export const iAmAdmin = $i != null && $i.isAdmin; export const iAmAdmin = $i != null && $i.isAdmin;
export function signinRequired() {
if ($i == null) throw new Error('signin required');
return $i;
}
export let notesCount = $i == null ? 0 : $i.notesCount; export let notesCount = $i == null ? 0 : $i.notesCount;
export function incNotesCount() { export function incNotesCount() {
notesCount++; notesCount++;
@ -246,7 +252,7 @@ export async function openAccountMenu(opts: {
} }
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
function createItem(account: Misskey.entities.UserDetailed) { function createItem(account: Misskey.entities.UserDetailed) {
return { return {

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