Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	packages/backend/src/models/RepositoryModule.ts
#	packages/backend/src/models/_.ts
#	packages/backend/src/postgres.ts
#	packages/frontend/src/components/MkDrive.file.vue
#	packages/frontend/src/components/MkEmojiPicker.vue
#	packages/frontend/src/pages/admin/index.vue
#	packages/frontend/src/pages/user/home.vue
#	packages/frontend/src/router.ts
#	packages/frontend/src/ui/universal.vue
This commit is contained in:
mattyatea 2024-01-11 21:46:28 +09:00
commit 33507e24ff
157 changed files with 3973 additions and 2059 deletions

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

@ -5,7 +5,19 @@ on:
branches: branches:
- master - master
- develop - 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:

View File

@ -5,10 +5,18 @@ on:
branches: branches:
- master - master
- develop - 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 +59,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.10.0]
services:
postgres:
image: postgres:15
ports:
- 54312:5432
env:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
ports:
- 56312:6379
steps:
- uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- run: corepack enable
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
run: git diff --exit-code pnpm-lock.yaml
- name: Copy Configure
run: cp .github/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

@ -5,7 +5,20 @@ on:
branches: branches:
- master - master
- develop - 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:

View File

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

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

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

1
.gitignore vendored
View File

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

View File

@ -21,14 +21,21 @@
### Client ### Client
- Feat: 新しいゲームを追加 - Feat: 新しいゲームを追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように
- Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md)
- 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
- Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: ネイティブモードの絵文字がモノクロにならないように
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
### Server ### Server
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
- Enhance: クリップをエクスポートできるように - Enhance: クリップをエクスポートできるように
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
## 2023.12.2 ## 2023.12.2

24
locales/index.d.ts vendored
View File

@ -657,6 +657,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;
@ -1236,6 +1237,20 @@ 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;
"ranking": string;
"_bubbleGame": {
"howToPlay": string;
"_howToPlay": {
"section1": string;
"section2": string;
"section3": string;
};
};
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;
@ -1700,6 +1715,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": {

View File

@ -654,6 +654,7 @@ medium: "中"
small: "小" small: "小"
generateAccessToken: "アクセストークンの発行" generateAccessToken: "アクセストークンの発行"
permission: "権限" permission: "権限"
adminPermission: "管理者権限"
enableAll: "全て有効にする" enableAll: "全て有効にする"
disableAll: "全て無効にする" disableAll: "全て無効にする"
tokenRequested: "アカウントへのアクセス許可" tokenRequested: "アカウントへのアクセス許可"
@ -1233,6 +1234,19 @@ decorate: "デコる"
addMfmFunction: "装飾を追加" addMfmFunction: "装飾を追加"
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する" enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
bubbleGame: "バブルゲーム" bubbleGame: "バブルゲーム"
sfx: "効果音"
soundWillBePlayed: "サウンドが再生されます"
showReplay: "リプレイを見る"
replay: "リプレイ"
replaying: "リプレイ中"
ranking: "ランキング"
_bubbleGame:
howToPlay: "遊び方"
_howToPlay:
section1: "位置を調整してハコにモノを落とします。"
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
section3: "モノがハコからあふれるとゲームオーバーです。ハコからあふれないようにしつつモノを融合させてハイスコアを目指そう!"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
@ -1611,6 +1625,13 @@ _achievements:
_tutorialCompleted: _tutorialCompleted:
title: "Misskey初心者講座 修了証" title: "Misskey初心者講座 修了証"
description: "チュートリアルを完了した" description: "チュートリアルを完了した"
_bubbleGameExplodingHead:
title: "🤯"
description: "バブルゲームで最も大きいモノを出した"
_bubbleGameDoubleExplodingHead:
title: "ダブル🤯"
description: "バブルゲームで最も大きいモを2つ同時に出した"
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
_role: _role:
new: "ロールの作成" new: "ロールの作成"

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,24 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class BubbleGameRecord1704959805077 {
name = 'BubbleGameRecord1704959805077'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `);
await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `);
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`);
await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`);
await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`);
await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`);
await queryRunner.query(`DROP TABLE "bubble_game_record"`);
}
}

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": {
@ -178,6 +183,7 @@
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "^1.0.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",
@ -226,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 { Global, Inject, Module } from '@nestjs/common'; import { Global, Inject, Module } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { DataSource } from 'typeorm'; import { DataSource } from 'typeorm';
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
import { Config, loadConfig } from './config.js'; import { Config, loadConfig } from './config.js';
import { createPostgresDataSource } from './postgres.js'; import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js'; import { RepositoryModule } from './models/RepositoryModule.js';
import { allSettled } from './misc/promise-tracker.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common'; import type { Provider, OnApplicationShutdown } from '@nestjs/common';
const $config: Provider = { const $config: Provider = {
@ -94,14 +94,9 @@ export class GlobalModule implements OnApplicationShutdown {
) { } ) { }
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

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

@ -55,6 +55,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';
@ -644,7 +645,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, ScheduleNotePostJobData } from '../queue/types.js'; import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, ScheduleNotePostJobData } from '../queue/types.js';
@ -116,14 +116,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

@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js'; import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
const FALLBACK = '❤'; const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@ -268,7 +269,7 @@ export class ReactionService {
} }
} }
dm.execute(); trackPromise(dm.execute());
} }
//#endregion //#endregion
} }
@ -316,7 +317,7 @@ export class ReactionService {
dm.addDirectRecipe(reactee as MiRemoteUser); dm.addDirectRecipe(reactee as MiRemoteUser);
} }
dm.addFollowersRecipe(); dm.addFollowersRecipe();
dm.execute(); trackPromise(dm.execute());
} }
//#endregion //#endregion
} }

View File

@ -144,7 +144,7 @@ class DeliverManager {
} }
// deliver // deliver
this.queueService.deliverMany(this.actor, this.activity, inboxes); await this.queueService.deliverMany(this.actor, this.activity, inboxes);
} }
} }

View File

@ -80,5 +80,6 @@ export const DI = {
flashsRepository: Symbol('flashsRepository'), flashsRepository: Symbol('flashsRepository'),
flashLikesRepository: Symbol('flashLikesRepository'), flashLikesRepository: Symbol('flashLikesRepository'),
userMemosRepository: Symbol('userMemosRepository'), userMemosRepository: Symbol('userMemosRepository'),
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
//#endregion //#endregion
}; };

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

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@Entity('bubble_game_record')
export class MiBubbleGameRecord {
@PrimaryColumn(id())
public id: string;
@Index()
@Column({
...id(),
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: MiUser | null;
@Index()
@Column('timestamp with time zone')
public seededAt: Date;
@Column('varchar', {
length: 1024,
})
public seed: string;
@Column('integer')
public gameVersion: number;
@Column('varchar', {
length: 128,
})
public gameMode: string;
@Index()
@Column('integer')
public score: number;
@Column('jsonb', {
default: [],
})
public logs: any[];
@Column('boolean', {
default: false,
})
public isVerified: boolean;
}

View File

@ -72,6 +72,7 @@ import {
MiUserSecurityKey, MiUserSecurityKey,
MiWebhook, MiWebhook,
MiScheduledNote, MiScheduledNote,
MiBubbleGameRecord
} from './_.js'; } from './_.js';
import type { DataSource } from 'typeorm'; import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -478,6 +479,12 @@ const $userMemosRepository: Provider = {
inject: [DI.db], inject: [DI.db],
}; };
export const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
inject: [DI.db],
};
@Module({ @Module({
imports: [ imports: [
], ],
@ -549,6 +556,7 @@ const $userMemosRepository: Provider = {
$flashsRepository, $flashsRepository,
$flashLikesRepository, $flashLikesRepository,
$userMemosRepository, $userMemosRepository,
$bubbleGameRecordsRepository,
], ],
exports: [ exports: [
$usersRepository, $usersRepository,
@ -618,6 +626,7 @@ const $userMemosRepository: Provider = {
$flashsRepository, $flashsRepository,
$flashLikesRepository, $flashLikesRepository,
$userMemosRepository, $userMemosRepository,
$bubbleGameRecordsRepository,
], ],
}) })
export class RepositoryModule {} export class RepositoryModule {}

View File

@ -69,6 +69,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
import { MiFlash } from '@/models/Flash.js'; import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js'; import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiScheduledNote } from './ScheduledNote.js'; import { MiScheduledNote } from './ScheduledNote.js';
import type { Repository } from 'typeorm'; import type { Repository } from 'typeorm';
@ -140,6 +141,7 @@ export {
MiFlash, MiFlash,
MiFlashLike, MiFlashLike,
MiUserMemo, MiUserMemo,
MiBubbleGameRecord,
}; };
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>; export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
@ -209,3 +211,4 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
export type FlashsRepository = Repository<MiFlash>; export type FlashsRepository = Repository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike>; export type FlashLikesRepository = Repository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo>; export type UserMemoRepository = Repository<MiUserMemo>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;

View File

@ -78,6 +78,7 @@ import { MiFlash } from '@/models/Flash.js';
import { MiFlashLike } from '@/models/FlashLike.js'; import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserMemo } from '@/models/UserMemo.js'; import { MiUserMemo } from '@/models/UserMemo.js';
import { MiScheduledNote } from '@/models/ScheduledNote.js'; import { MiScheduledNote } from '@/models/ScheduledNote.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { Config } from '@/config.js'; import { Config } from '@/config.js';
import MisskeyLogger from '@/logger.js'; import MisskeyLogger from '@/logger.js';
@ -194,6 +195,7 @@ export const entities = [
MiFlash, MiFlash,
MiFlashLike, MiFlashLike,
MiUserMemo, MiUserMemo,
MiBubbleGameRecord,
...charts, ...charts,
]; ];

View File

@ -374,6 +374,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
import { GetterService } from './GetterService.js'; import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js'; import { ApiLoggerService } from './ApiLoggerService.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
@ -746,6 +748,8 @@ const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass:
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default }; const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default }; const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
@Module({ @Module({
imports: [ imports: [
@ -1122,6 +1126,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$fetchRss, $fetchRss,
$fetchExternalResources, $fetchExternalResources,
$retention, $retention,
$bubbleGame_register,
$bubbleGame_ranking,
], ],
exports: [ exports: [
$admin_meta, $admin_meta,
@ -1489,6 +1495,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$fetchRss, $fetchRss,
$fetchExternalResources, $fetchExternalResources,
$retention, $retention,
$bubbleGame_register,
$bubbleGame_ranking,
], ],
}) })
export class EndpointsModule {} export class EndpointsModule {}

View File

@ -374,6 +374,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js'; import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js'; import * as ep___retention from './endpoints/retention.js';
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
const eps = [ const eps = [
['admin/meta', ep___admin_meta], ['admin/meta', ep___admin_meta],
@ -744,6 +746,8 @@ const eps = [
['fetch-rss', ep___fetchRss], ['fetch-rss', ep___fetchRss],
['fetch-external-resources', ep___fetchExternalResources], ['fetch-external-resources', ep___fetchExternalResources],
['retention', ep___retention], ['retention', ep___retention],
['bubble-game/register', ep___bubbleGame_register],
['bubble-game/ranking', ep___bubbleGame_ranking],
]; ];
interface IEndpointMetaBase { interface IEndpointMetaBase {

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,75 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { MoreThan } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { BubbleGameRecordsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
export const meta = {
tags: [],
allowGet: true,
cacheSec: 60,
errors: {
},
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: { type: 'string', format: 'misskey:id' },
score: { type: 'integer' },
user: { ref: 'UserLite' },
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
gameMode: { type: 'string' },
},
required: ['gameMode'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.bubbleGameRecordsRepository)
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps) => {
const records = await this.bubbleGameRecordsRepository.find({
where: {
gameMode: ps.gameMode,
seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)),
},
order: {
score: 'DESC',
},
take: 10,
relations: ['user'],
});
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
return records.map(r => ({
id: r.id,
score: r.score,
user: users.find(u => u.id === r.user!.id),
}));
});
}
}

View File

@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { IdService } from '@/core/IdService.js';
import type { BubbleGameRecordsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: [],
requireCredential: true,
kind: 'write:account',
limit: {
duration: ms('1hour'),
max: 120,
minInterval: ms('30sec'),
},
errors: {
invalidSeed: {
message: 'Provided seed is invalid.',
code: 'INVALID_SEED',
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
},
},
res: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
score: { type: 'integer', minimum: 0 },
seed: { type: 'string', minLength: 1, maxLength: 1024 },
logs: { type: 'array' },
gameMode: { type: 'string' },
gameVersion: { type: 'integer' },
},
required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.bubbleGameRecordsRepository)
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const seedDate = new Date(parseInt(ps.seed, 10));
const now = new Date();
// シードが未来なのは通常のプレイではありえないので弾く
if (seedDate.getTime() > now.getTime()) {
throw new ApiError(meta.errors.invalidSeed);
}
// シードが古すぎる(1時間以上前)のも弾く
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) {
throw new ApiError(meta.errors.invalidSeed);
}
await this.bubbleGameRecordsRepository.insert({
id: this.idService.gen(now.getTime()),
seed: ps.seed,
seededAt: seedDate,
userId: me.id,
score: ps.score,
logs: ps.logs,
gameMode: ps.gameMode,
gameVersion: ps.gameVersion,
isVerified: false,
});
});
}
}

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,11 +19,9 @@ 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.SignupResponse;
const config = loadConfig(); const config = loadConfig();
@ -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));
@ -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,21 +6,10 @@
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
/** ヒロイン */ /** ヒロイン */

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.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; 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();
});
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.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; 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 alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; let carol: misskey.entities.SignupResponse;
let dave: misskey.entities.SignupResponse; let dave: 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' });
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

@ -6,12 +6,12 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { signup, api, startServer, startJobQueue, port, post } from '../utils.js'; import { api, port, post, signup, startJobQueue } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common'; import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('export-clips', () => { describe('export-clips', () => {
let app: INestApplicationContext; let queue: INestApplicationContext;
let alice: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
@ -33,14 +33,13 @@ describe('export-clips', () => {
} }
beforeAll(async () => { beforeAll(async () => {
app = await startServer(); queue = await startJobQueue();
await startJobQueue();
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 () => { afterAll(async () => {
await app.close(); await queue.close();
}); });
beforeEach(async () => { beforeEach(async () => {

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,8 +22,6 @@ 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.SignupResponse;
let aliceUploadedFile: any; let aliceUploadedFile: any;
let alicesPost: any; let alicesPost: any;
@ -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 alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: 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' });
}, 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,19 +3,19 @@
* 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;
@ -30,8 +30,8 @@ describe('Account Move', () => {
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.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; 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.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; 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}`;
@ -147,7 +152,6 @@ 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.SignupResponse; let alice: misskey.entities.SignupResponse;
@ -156,7 +160,6 @@ describe('OAuth', () => {
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.SignupResponse; let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; 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) => {
@ -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 alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse; 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('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.SignupResponse;
let jpgNote: any; let jpgNote: any;
let pngNote: any; let pngNote: any;
let jpgPngNote: any; let jpgPngNote: any;
beforeAll(async () => { beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
@ -34,10 +30,6 @@ describe('users/notes', () => {
}); });
}, 1000 * 60 * 2); }, 1000 * 60 * 2);
afterAll(async() => {
await app.close();
});
test('withFiles', async () => { test('withFiles', async () => {
const res = await api('/users/notes', { const res = await api('/users/notes', {
userId: alice.id, userId: alice.id,

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

@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { AnnouncementService } from '@/core/AnnouncementService.js'; import { AnnouncementService } from '@/core/AnnouncementService.js';
import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js'; import type {
AnnouncementReadsRepository,
AnnouncementsRepository,
MiAnnouncement,
MiUser,
UsersRepository,
} from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { genAidx } from '@/misc/id/aidx.js'; import { genAidx } from '@/misc/id/aidx.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';

View File

@ -6,7 +6,13 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3'; import {
DeleteObjectCommand,
DeleteObjectCommandOutput,
InvalidObjectState,
NoSuchKey,
S3Client,
} from '@aws-sdk/client-s3';
import { mockClient } from 'aws-sdk-client-mock'; import { mockClient } from 'aws-sdk-client-mock';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { DriveService } from '@/core/DriveService.js'; import { DriveService } from '@/core/DriveService.js';

View File

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

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

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 248 KiB

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",
@ -24,7 +24,7 @@
"@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/pluginutils": "5.1.0", "@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.16.0", "@syuilo/aiscript": "0.17.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": "5.0.2", "@vitejs/plugin-vue": "5.0.2",
@ -58,6 +58,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",

View File

@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js'; import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/global/router/definition.js';
export async function common(createVue: () => App<Element>) { export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`); console.info(`Misskey v${version}`);
@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
const app = createVue(); const app = createVue();
setupRouter(app);
if (_DEV_) { if (_DEV_) {
app.config.performance = true; app.config.performance = true;
} }

View File

@ -3,23 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { createApp, markRaw, defineAsyncComponent } from 'vue'; import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import { ui } from '@/config.js'; import { ui } from '@/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { confirm, alert, post, popup, toast } from '@/os.js'; import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, updateAccount, signout } from '@/account.js'; import { $i, signout, updateAccount } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js'; import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/global/router/main.js';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
@ -271,7 +271,7 @@ export async function mainBoot() {
main.on('unreadAntenna', () => { main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true }); updateAccount({ hasUnreadAntenna: true });
sound.play('antenna'); sound.playMisskeySfx('antenna');
}); });
main.on('readAllAnnouncements', () => { main.on('readAllAnnouncements', () => {

View File

@ -266,14 +266,23 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
} }
const matched = new Map<string, EmojiScore>(); const matched = new Map<string, EmojiScore>();
//
emojiDb.some(x => {
if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
}
return matched.size === max;
});
// //
if (matched.size < max) {
emojiDb.some(x => { emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf) { if (x.name.startsWith(query) && !x.aliasOf) {
matched.set(x.name, { emoji: x, score: query.length + 1 }); matched.set(x.name, { emoji: x, score: query.length + 1 });
} }
return matched.size === max; return matched.size === max;
}); });
}
// //
if (matched.size < max) { if (matched.size < max) {

View File

@ -45,10 +45,10 @@ import bytes from '@/filters/bytes.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { useRouter } from '@/router.js';
import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js'; import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import { useRouter } from '@/global/router/supplier.js';
const router = useRouter(); const router = useRouter();

View File

@ -226,7 +226,18 @@ watch(q, () => {
} }
} }
} else { } else {
if (customEmojisMap.has(newQ)) {
matches.add(customEmojisMap.get(newQ)!);
}
if (matches.size >= max) return matches;
for (const emoji of emojis) { for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias === newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) { if (emoji.name.startsWith(newQ)) {
matches.add(emoji); matches.add(emoji);
if (matches.size >= max) break; if (matches.size >= max) break;

View File

@ -352,7 +352,7 @@ function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.play('reaction'); sound.playMisskeySfx('reaction');
if (props.mock) { if (props.mock) {
return; return;
@ -372,7 +372,7 @@ function react(viaKeyboard = false): void {
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value, reaction => {
sound.play('reaction'); sound.playMisskeySfx('reaction');
if (props.mock) { if (props.mock) {
emit('reaction', reaction); emit('reaction', reaction);

View File

@ -396,7 +396,7 @@ function react(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
showMovedDialog(); showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
sound.play('reaction'); sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id, noteId: appearNote.value.id,
@ -412,7 +412,7 @@ function react(viaKeyboard = false): void {
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value, reaction => {
sound.play('reaction'); sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id, noteId: appearNote.value.id,

View File

@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<div ref="contents" :class="$style.root" style="container-type: inline-size;"> <div ref="contents" :class="$style.root" style="container-type: inline-size;">
<RouterView :key="reloadCount" :router="router"/> <RouterView :key="reloadCount" :router="windowRouter"/>
</div> </div>
</MkWindow> </MkWindow>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue'; import { computed, ComputedRef, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js'; import { popout as _popout } from '@/scripts/popout.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { mainRouter, routes, page } from '@/router.js'; import { useScrollPositionManager } from '@/nirax.js';
import { $i } from '@/account.js';
import { Router, useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js'; import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollContainer } from '@/scripts/scroll.js';
import { useRouterFactory } from '@/global/router/supplier.js';
import { mainRouter } from '@/global/router/main.js';
const props = defineProps<{ const props = defineProps<{
initialPath: string; initialPath: string;
@ -52,14 +52,15 @@ defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue'))); const routerFactory = useRouterFactory();
const windowRouter = routerFactory(props.initialPath);
const contents = shallowRef<HTMLElement>(); const contents = shallowRef<HTMLElement>();
const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = ref<{ path: string; key: any; }[]>([{ const history = ref<{ path: string; key: any; }[]>([{
path: router.getCurrentPath(), path: windowRouter.getCurrentPath(),
key: router.getCurrentKey(), key: windowRouter.getCurrentKey(),
}]); }]);
const buttonsLeft = computed(() => { const buttonsLeft = computed(() => {
const buttons = []; const buttons = [];
@ -88,11 +89,11 @@ const buttonsRight = computed(() => {
}); });
const reloadCount = ref(0); const reloadCount = ref(0);
router.addListener('push', ctx => { windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.path, key: ctx.key }); history.value.push({ path: ctx.path, key: ctx.key });
}); });
provide('router', router); provide('router', windowRouter);
provideMetadataReceiver((info) => { provideMetadataReceiver((info) => {
pageMetadata.value = info; pageMetadata.value = info;
}); });
@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{
icon: 'ti ti-external-link', icon: 'ti ti-external-link',
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(url + router.getCurrentPath(), '_blank', 'noopener'); window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
windowEl.value.close(); windowEl.value.close();
}, },
}, { }, {
icon: 'ti ti-link', icon: 'ti ti-link',
text: i18n.ts.copyLink, text: i18n.ts.copyLink,
action: () => { action: () => {
copyToClipboard(url + router.getCurrentPath()); copyToClipboard(url + windowRouter.getCurrentPath());
}, },
}])); }]));
function back() { function back() {
history.value.pop(); history.value.pop();
router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key); windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
} }
function reload() { function reload() {
@ -137,16 +138,16 @@ function close() {
} }
function expand() { function expand() {
mainRouter.push(router.getCurrentPath(), 'forcePage'); mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
windowEl.value.close(); windowEl.value.close();
} }
function popout() { function popout() {
_popout(router.getCurrentPath(), windowEl.value.$el); _popout(windowRouter.getCurrentPath(), windowEl.value.$el);
windowEl.value.close(); windowEl.value.close();
} }
useScrollPositionManager(() => getScrollContainer(contents.value), router); useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
onMounted(() => { onMounted(() => {
openingWindowsCount.value++; openingWindowsCount.value++;

View File

@ -47,6 +47,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: number): void; (ev: 'update:modelValue', value: number): void;
(ev: 'dragEnded', value: number): void;
}>(); }>();
const containerEl = shallowRef<HTMLElement>(); const containerEl = shallowRef<HTMLElement>();
@ -147,6 +148,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
// //
if (beforeValue !== finalValue.value) { if (beforeValue !== finalValue.value) {
emit('update:modelValue', finalValue.value); emit('update:modelValue', finalValue.value);
emit('dragEnded', finalValue.value);
} }
}; };

View File

@ -64,7 +64,7 @@ async function toggleReaction() {
if (confirm.canceled) return; if (confirm.canceled) return;
if (oldReaction !== props.reaction) { if (oldReaction !== props.reaction) {
sound.play('reaction'); sound.playMisskeySfx('reaction');
} }
if (mock) { if (mock) {
@ -83,7 +83,7 @@ async function toggleReaction() {
} }
}); });
} else { } else {
sound.play('reaction'); sound.playMisskeySfx('reaction');
if (mock) { if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1)); emit('reactionToggled', props.reaction, (props.count + 1));

View File

@ -81,7 +81,7 @@ function prepend(note) {
emit('note'); emit('note');
if (props.sound) { if (props.sound) {
sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note'); sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
} }
} }

View File

@ -33,7 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton> <MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div> </div>
<div class="_gaps_s"> <div class="_gaps_s">
<MkSwitch v-for="kind in Object.keys(permissions)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch> <MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
</div>
<div v-if="iAmAdmin" :class="$style.adminPermissions">
<div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
<div class="_gaps_s">
<MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
</div>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
@ -49,6 +55,7 @@ import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue'; import MkInfo from './MkInfo.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { iAmAdmin } from '@/account.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
title?: string | null; title?: string | null;
@ -68,37 +75,76 @@ const emit = defineEmits<{
}>(); }>();
const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin')); const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin'));
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const name = ref(props.initialName); const name = ref(props.initialName);
const permissions = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{}); const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
if (props.initialPermissions) { if (props.initialPermissions) {
for (const kind of props.initialPermissions) { for (const kind of props.initialPermissions) {
permissions.value[kind] = true; permissionSwitches.value[kind] = true;
} }
} else { } else {
for (const kind of defaultPermissions) { for (const kind of defaultPermissions) {
permissions.value[kind] = false; permissionSwitches.value[kind] = false;
}
if (iAmAdmin) {
for (const kind of adminPermissions) {
permissionSwitchesForAdmin.value[kind] = false;
}
} }
} }
function ok(): void { function ok(): void {
emit('done', { emit('done', {
name: name.value, name: name.value,
permissions: Object.keys(permissions.value).filter(p => permissions.value[p]), permissions: [
...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]),
...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []),
],
}); });
dialog.value?.close(); dialog.value?.close();
} }
function disableAll(): void { function disableAll(): void {
for (const p in permissions.value) { for (const p in permissionSwitches.value) {
permissions.value[p] = false; permissionSwitches.value[p] = false;
}
if (iAmAdmin) {
for (const p in permissionSwitchesForAdmin.value) {
permissionSwitchesForAdmin.value[p] = false;
}
} }
} }
function enableAll(): void { function enableAll(): void {
for (const p in permissions.value) { for (const p in permissionSwitches.value) {
permissions.value[p] = true; permissionSwitches.value[p] = true;
}
if (iAmAdmin) {
for (const p in permissionSwitchesForAdmin.value) {
permissionSwitchesForAdmin.value[p] = true;
}
} }
} }
</script> </script>
<style module lang="scss">
.adminPermissions {
margin: 8px -6px 0;
padding: 24px 6px 6px;
border: 2px solid var(--error);
border-radius: calc(var(--radius) / 2);
}
.adminPermissionsHeader {
margin: -34px 0 6px 12px;
padding: 0 4px;
width: fit-content;
color: var(--error);
background: var(--panel);
}
</style>

View File

@ -15,7 +15,7 @@ import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/global/router/supplier.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
to: string; to: string;

View File

@ -99,7 +99,7 @@ function onClick(ev: MouseEvent) {
icon: 'ti ti-plus', icon: 'ti ti-plus',
action: () => { action: () => {
react(`:${props.name}:`); react(`:${props.name}:`);
sound.play('reaction'); sound.playMisskeySfx('reaction');
}, },
}] : [])], ev.currentTarget ?? ev.target); }] : [])], ev.currentTarget ?? ev.target);
} }

View File

@ -55,7 +55,7 @@ function onClick(ev: MouseEvent) {
icon: 'ti ti-plus', icon: 'ti ti-plus',
action: () => { action: () => {
react(props.emoji); react(props.emoji);
sound.play('reaction'); sound.playMisskeySfx('reaction');
}, },
}] : [])], ev.currentTarget ?? ev.target); }] : [])], ev.currentTarget ?? ev.target);
} }

View File

@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue'; import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
import { Resolved, Router } from '@/nirax.js'; import { IRouter, Resolved } from '@/nirax.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const props = defineProps<{ const props = defineProps<{
router?: Router; router?: IRouter;
}>(); }>();
const router = props.router ?? inject('router'); const router = props.router ?? inject('router');

View File

@ -0,0 +1,571 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
import { IRouter, Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
import { setMainRouter } from '@/global/router/main.js';
const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
loader: loader,
loadingComponent: MkLoading,
errorComponent: MkError,
});
const routes = [{
path: '/@:initUser/pages/:initPageName/view-source',
component: page(() => import('@/pages/page-editor/page-editor.vue')),
}, {
path: '/@:username/pages/:pageName',
component: page(() => import('@/pages/page.vue')),
}, {
path: '/@:acct/following',
component: page(() => import('@/pages/user/following.vue')),
}, {
path: '/@:acct/followers',
component: page(() => import('@/pages/user/followers.vue')),
}, {
name: 'user',
path: '/@:acct/:page?',
component: page(() => import('@/pages/user/index.vue')),
}, {
name: 'note',
path: '/notes/:noteId',
component: page(() => import('@/pages/note.vue')),
}, {
name: 'list',
path: '/list/:listId',
component: page(() => import('@/pages/list.vue')),
}, {
path: '/clips/:clipId',
component: page(() => import('@/pages/clip.vue')),
}, {
path: '/instance-info/:host',
component: page(() => import('@/pages/instance-info.vue')),
}, {
name: 'settings',
path: '/settings',
component: page(() => import('@/pages/settings/index.vue')),
loginRequired: true,
children: [{
path: '/profile',
name: 'profile',
component: page(() => import('@/pages/settings/profile.vue')),
}, {
path: '/avatar-decoration',
name: 'avatarDecoration',
component: page(() => import('@/pages/settings/avatar-decoration.vue')),
}, {
path: '/roles',
name: 'roles',
component: page(() => import('@/pages/settings/roles.vue')),
}, {
path: '/privacy',
name: 'privacy',
component: page(() => import('@/pages/settings/privacy.vue')),
}, {
path: '/emoji-picker',
name: 'emojiPicker',
component: page(() => import('@/pages/settings/emoji-picker.vue')),
}, {
path: '/drive',
name: 'drive',
component: page(() => import('@/pages/settings/drive.vue')),
}, {
path: '/drive/cleaner',
name: 'drive',
component: page(() => import('@/pages/settings/drive-cleaner.vue')),
}, {
path: '/notifications',
name: 'notifications',
component: page(() => import('@/pages/settings/notifications.vue')),
}, {
path: '/email',
name: 'email',
component: page(() => import('@/pages/settings/email.vue')),
}, {
path: '/security',
name: 'security',
component: page(() => import('@/pages/settings/security.vue')),
}, {
path: '/general',
name: 'general',
component: page(() => import('@/pages/settings/general.vue')),
}, {
path: '/theme/install',
name: 'theme',
component: page(() => import('@/pages/settings/theme.install.vue')),
}, {
path: '/theme/manage',
name: 'theme',
component: page(() => import('@/pages/settings/theme.manage.vue')),
}, {
path: '/theme',
name: 'theme',
component: page(() => import('@/pages/settings/theme.vue')),
}, {
path: '/navbar',
name: 'navbar',
component: page(() => import('@/pages/settings/navbar.vue')),
}, {
path: '/statusbar',
name: 'statusbar',
component: page(() => import('@/pages/settings/statusbar.vue')),
}, {
path: '/sounds',
name: 'sounds',
component: page(() => import('@/pages/settings/sounds.vue')),
}, {
path: '/plugin/install',
name: 'plugin',
component: page(() => import('@/pages/settings/plugin.install.vue')),
}, {
path: '/plugin',
name: 'plugin',
component: page(() => import('@/pages/settings/plugin.vue')),
}, {
path: '/import-export',
name: 'import-export',
component: page(() => import('@/pages/settings/import-export.vue')),
}, {
path: '/mute-block',
name: 'mute-block',
component: page(() => import('@/pages/settings/mute-block.vue')),
}, {
path: '/api',
name: 'api',
component: page(() => import('@/pages/settings/api.vue')),
}, {
path: '/apps',
name: 'api',
component: page(() => import('@/pages/settings/apps.vue')),
}, {
path: '/webhook/edit/:webhookId',
name: 'webhook',
component: page(() => import('@/pages/settings/webhook.edit.vue')),
}, {
path: '/webhook/new',
name: 'webhook',
component: page(() => import('@/pages/settings/webhook.new.vue')),
}, {
path: '/webhook',
name: 'webhook',
component: page(() => import('@/pages/settings/webhook.vue')),
}, {
path: '/deck',
name: 'deck',
component: page(() => import('@/pages/settings/deck.vue')),
}, {
path: '/preferences-backups',
name: 'preferences-backups',
component: page(() => import('@/pages/settings/preferences-backups.vue')),
}, {
path: '/migration',
name: 'migration',
component: page(() => import('@/pages/settings/migration.vue')),
}, {
path: '/custom-css',
name: 'general',
component: page(() => import('@/pages/settings/custom-css.vue')),
}, {
path: '/accounts',
name: 'profile',
component: page(() => import('@/pages/settings/accounts.vue')),
}, {
path: '/other',
name: 'other',
component: page(() => import('@/pages/settings/other.vue')),
}, {
path: '/',
component: page(() => import('@/pages/_empty_.vue')),
}],
}, {
path: '/reset-password/:token?',
component: page(() => import('@/pages/reset-password.vue')),
}, {
path: '/signup-complete/:code',
component: page(() => import('@/pages/signup-complete.vue')),
}, {
path: '/announcements',
component: page(() => import('@/pages/announcements.vue')),
}, {
path: '/about',
component: page(() => import('@/pages/about.vue')),
hash: 'initialTab',
}, {
path: '/about-misskey',
component: page(() => import('@/pages/about-misskey.vue')),
}, {
path: '/invite',
name: 'invite',
component: page(() => import('@/pages/invite.vue')),
}, {
path: '/ads',
component: page(() => import('@/pages/ads.vue')),
}, {
path: '/theme-editor',
component: page(() => import('@/pages/theme-editor.vue')),
loginRequired: true,
}, {
path: '/roles/:role',
component: page(() => import('@/pages/role.vue')),
}, {
path: '/user-tags/:tag',
component: page(() => import('@/pages/user-tag.vue')),
}, {
path: '/explore',
component: page(() => import('@/pages/explore.vue')),
hash: 'initialTab',
}, {
path: '/search',
component: page(() => import('@/pages/search.vue')),
query: {
q: 'query',
channel: 'channel',
type: 'type',
origin: 'origin',
},
}, {
path: '/authorize-follow',
component: page(() => import('@/pages/follow.vue')),
loginRequired: true,
}, {
path: '/share',
component: page(() => import('@/pages/share.vue')),
loginRequired: true,
}, {
path: '/api-console',
component: page(() => import('@/pages/api-console.vue')),
loginRequired: true,
}, {
path: '/scratchpad',
component: page(() => import('@/pages/scratchpad.vue')),
}, {
path: '/auth/:token',
component: page(() => import('@/pages/auth.vue')),
}, {
path: '/miauth/:session',
component: page(() => import('@/pages/miauth.vue')),
query: {
callback: 'callback',
name: 'name',
icon: 'icon',
permission: 'permission',
},
}, {
path: '/oauth/authorize',
component: page(() => import('@/pages/oauth.vue')),
}, {
path: '/tags/:tag',
component: page(() => import('@/pages/tag.vue')),
}, {
path: '/pages/new',
component: page(() => import('@/pages/page-editor/page-editor.vue')),
loginRequired: true,
}, {
path: '/pages/edit/:initPageId',
component: page(() => import('@/pages/page-editor/page-editor.vue')),
loginRequired: true,
}, {
path: '/pages',
component: page(() => import('@/pages/pages.vue')),
}, {
path: '/play/:id/edit',
component: page(() => import('@/pages/flash/flash-edit.vue')),
loginRequired: true,
}, {
path: '/play/new',
component: page(() => import('@/pages/flash/flash-edit.vue')),
loginRequired: true,
}, {
path: '/play/:id',
component: page(() => import('@/pages/flash/flash.vue')),
}, {
path: '/play',
component: page(() => import('@/pages/flash/flash-index.vue')),
}, {
path: '/gallery/:postId/edit',
component: page(() => import('@/pages/gallery/edit.vue')),
loginRequired: true,
}, {
path: '/gallery/new',
component: page(() => import('@/pages/gallery/edit.vue')),
loginRequired: true,
}, {
path: '/gallery/:postId',
component: page(() => import('@/pages/gallery/post.vue')),
}, {
path: '/gallery',
component: page(() => import('@/pages/gallery/index.vue')),
}, {
path: '/channels/:channelId/edit',
component: page(() => import('@/pages/channel-editor.vue')),
loginRequired: true,
}, {
path: '/channels/new',
component: page(() => import('@/pages/channel-editor.vue')),
loginRequired: true,
}, {
path: '/channels/:channelId',
component: page(() => import('@/pages/channel.vue')),
}, {
path: '/channels',
component: page(() => import('@/pages/channels.vue')),
}, {
path: '/custom-emojis-manager',
component: page(() => import('@/pages/custom-emojis-manager.vue')),
}, {
path: '/avatar-decorations',
name: 'avatarDecorations',
component: page(() => import('@/pages/avatar-decorations.vue')),
}, {
path: '/registry/keys/:domain/:path(*)?',
component: page(() => import('@/pages/registry.keys.vue')),
}, {
path: '/registry/value/:domain/:path(*)?',
component: page(() => import('@/pages/registry.value.vue')),
}, {
path: '/registry',
component: page(() => import('@/pages/registry.vue')),
}, {
path: '/install-extentions',
component: page(() => import('@/pages/install-extentions.vue')),
loginRequired: true,
}, {
path: '/admin/user/:userId',
component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.vue')),
}, {
path: '/admin/file/:fileId',
component: iAmModerator ? page(() => import('@/pages/admin-file.vue')) : page(() => import('@/pages/not-found.vue')),
}, {
path: '/admin',
component: iAmModerator ? page(() => import('@/pages/admin/index.vue')) : page(() => import('@/pages/not-found.vue')),
children: [{
path: '/overview',
name: 'overview',
component: page(() => import('@/pages/admin/overview.vue')),
}, {
path: '/users',
name: 'users',
component: page(() => import('@/pages/admin/users.vue')),
}, {
path: '/emojis',
name: 'emojis',
component: page(() => import('@/pages/custom-emojis-manager.vue')),
}, {
path: '/avatar-decorations',
name: 'avatarDecorations',
component: page(() => import('@/pages/avatar-decorations.vue')),
}, {
path: '/queue',
name: 'queue',
component: page(() => import('@/pages/admin/queue.vue')),
}, {
path: '/files',
name: 'files',
component: page(() => import('@/pages/admin/files.vue')),
}, {
path: '/federation',
name: 'federation',
component: page(() => import('@/pages/admin/federation.vue')),
}, {
path: '/announcements',
name: 'announcements',
component: page(() => import('@/pages/admin/announcements.vue')),
}, {
path: '/ads',
name: 'ads',
component: page(() => import('@/pages/admin/ads.vue')),
}, {
path: '/roles/:id/edit',
name: 'roles',
component: page(() => import('@/pages/admin/roles.edit.vue')),
}, {
path: '/roles/new',
name: 'roles',
component: page(() => import('@/pages/admin/roles.edit.vue')),
}, {
path: '/roles/:id',
name: 'roles',
component: page(() => import('@/pages/admin/roles.role.vue')),
}, {
path: '/roles',
name: 'roles',
component: page(() => import('@/pages/admin/roles.vue')),
}, {
path: '/database',
name: 'database',
component: page(() => import('@/pages/admin/database.vue')),
}, {
path: '/abuses',
name: 'abuses',
component: page(() => import('@/pages/admin/abuses.vue')),
}, {
path: '/modlog',
name: 'modlog',
component: page(() => import('@/pages/admin/modlog.vue')),
}, {
path: '/settings',
name: 'settings',
component: page(() => import('@/pages/admin/settings.vue')),
}, {
path: '/branding',
name: 'branding',
component: page(() => import('@/pages/admin/branding.vue')),
}, {
path: '/moderation',
name: 'moderation',
component: page(() => import('@/pages/admin/moderation.vue')),
}, {
path: '/email-settings',
name: 'email-settings',
component: page(() => import('@/pages/admin/email-settings.vue')),
}, {
path: '/object-storage',
name: 'object-storage',
component: page(() => import('@/pages/admin/object-storage.vue')),
}, {
path: '/security',
name: 'security',
component: page(() => import('@/pages/admin/security.vue')),
}, {
path: '/relays',
name: 'relays',
component: page(() => import('@/pages/admin/relays.vue')),
}, {
path: '/instance-block',
name: 'instance-block',
component: page(() => import('@/pages/admin/instance-block.vue')),
}, {
path: '/proxy-account',
name: 'proxy-account',
component: page(() => import('@/pages/admin/proxy-account.vue')),
}, {
path: '/external-services',
name: 'external-services',
component: page(() => import('@/pages/admin/external-services.vue')),
}, {
path: '/other-settings',
name: 'other-settings',
component: page(() => import('@/pages/admin/other-settings.vue')),
}, {
path: '/server-rules',
name: 'server-rules',
component: page(() => import('@/pages/admin/server-rules.vue')),
}, {
path: '/invites',
name: 'invites',
component: page(() => import('@/pages/admin/invites.vue')),
}, {
path: '/',
component: page(() => import('@/pages/_empty_.vue')),
}],
}, {
path: '/my/notifications',
component: page(() => import('@/pages/notifications.vue')),
loginRequired: true,
}, {
path: '/my/favorites',
component: page(() => import('@/pages/favorites.vue')),
loginRequired: true,
}, {
path: '/my/achievements',
component: page(() => import('@/pages/achievements.vue')),
loginRequired: true,
}, {
path: '/my/drive/folder/:folder',
component: page(() => import('@/pages/drive.vue')),
loginRequired: true,
}, {
path: '/my/drive',
component: page(() => import('@/pages/drive.vue')),
loginRequired: true,
}, {
path: '/my/drive/file/:fileId',
component: page(() => import('@/pages/drive.file.vue')),
loginRequired: true,
}, {
path: '/my/follow-requests',
component: page(() => import('@/pages/follow-requests.vue')),
loginRequired: true,
}, {
path: '/my/lists/:listId',
component: page(() => import('@/pages/my-lists/list.vue')),
loginRequired: true,
}, {
path: '/my/lists',
component: page(() => import('@/pages/my-lists/index.vue')),
loginRequired: true,
}, {
path: '/my/clips',
component: page(() => import('@/pages/my-clips/index.vue')),
loginRequired: true,
}, {
path: '/my/antennas/create',
component: page(() => import('@/pages/my-antennas/create.vue')),
loginRequired: true,
}, {
path: '/my/antennas/:antennaId',
component: page(() => import('@/pages/my-antennas/edit.vue')),
loginRequired: true,
}, {
path: '/my/antennas',
component: page(() => import('@/pages/my-antennas/index.vue')),
loginRequired: true,
}, {
path: '/timeline/list/:listId',
component: page(() => import('@/pages/user-list-timeline.vue')),
loginRequired: true,
}, {
path: '/timeline/antenna/:antennaId',
component: page(() => import('@/pages/antenna-timeline.vue')),
loginRequired: true,
}, {
path: '/clicker',
component: page(() => import('@/pages/clicker.vue')),
loginRequired: true,
}, {
path: '/bubble-game',
component: page(() => import('@/pages/drop-and-fusion.vue')),
loginRequired: true,
}, {
path: '/timeline',
component: page(() => import('@/pages/timeline.vue')),
}, {
name: 'index',
path: '/',
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
globalCacheKey: 'index',
}, {
path: '/:(*)',
component: page(() => import('@/pages/not-found.vue')),
}];
function createRouterImpl(path: string): IRouter {
return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
}
/**
* {@link Router}{@link mainRouter}
* {@link Router}{@link provide}`routerFactory`
*/
export function setupRouter(app: App) {
app.provide('routerFactory', createRouterImpl);
const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
});
mainRouter.addListener('push', ctx => {
window.history.pushState({ key: ctx.key }, '', ctx.path);
});
setMainRouter(mainRouter);
}

View File

@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ShallowRef } from 'vue';
import { EventEmitter } from 'eventemitter3';
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
function getMainRouter(): IRouter {
const router = mainRouterHolder;
if (!router) {
throw new Error('mainRouter is not found.');
}
return router;
}
/**
*
* {@link setupRouter}
*/
export function setMainRouter(router: IRouter) {
if (mainRouterHolder) {
throw new Error('mainRouter is already exists.');
}
mainRouterHolder = router;
}
/**
* {@link mainRouter}
* {@link mainRouter}undefinedになる期間がある
* undefined込みにしたくないのでこのクラスを緩衝材として使用する
*/
class MainRouterProxy implements IRouter {
private supplier: () => IRouter;
constructor(supplier: () => IRouter) {
this.supplier = supplier;
}
get current(): Resolved {
return this.supplier().current;
}
get currentRef(): ShallowRef<Resolved> {
return this.supplier().currentRef;
}
get currentRoute(): ShallowRef<RouteDef> {
return this.supplier().currentRoute;
}
get navHook(): ((path: string, flag?: any) => boolean) | null {
return this.supplier().navHook;
}
set navHook(value) {
this.supplier().navHook = value;
}
getCurrentKey(): string {
return this.supplier().getCurrentKey();
}
getCurrentPath(): any {
return this.supplier().getCurrentPath();
}
push(path: string, flag?: any): void {
this.supplier().push(path, flag);
}
replace(path: string, key?: string | null): void {
this.supplier().replace(path, key);
}
resolve(path: string): Resolved | null {
return this.supplier().resolve(path);
}
eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
return this.supplier().eventNames();
}
listeners<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
): Array<EventEmitter.EventListener<RouterEvent, T>> {
return this.supplier().listeners(event);
}
listenerCount(
event: EventEmitter.EventNames<RouterEvent>,
): number {
return this.supplier().listenerCount(event);
}
emit<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
...args: EventEmitter.EventArgs<RouterEvent, T>
): boolean {
return this.supplier().emit(event, ...args);
}
on<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().on(event, fn, context);
return this;
}
addListener<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().addListener(event, fn, context);
return this;
}
once<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
): this {
this.supplier().once(event, fn, context);
return this;
}
removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn?: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
once?: boolean,
): this {
this.supplier().removeListener(event, fn, context, once);
return this;
}
off<T extends EventEmitter.EventNames<RouterEvent>>(
event: T,
fn?: EventEmitter.EventListener<RouterEvent, T>,
context?: any,
once?: boolean,
): this {
this.supplier().off(event, fn, context, once);
return this;
}
removeAllListeners(
event?: EventEmitter.EventNames<RouterEvent>,
): this {
this.supplier().removeAllListeners(event);
return this;
}
}
let mainRouterHolder: IRouter | null = null;
export const mainRouter: IRouter = new MainRouterProxy(getMainRouter);

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