Merge branch 'develop' into enhance/suggest-perfect-matched-emoji
|
@ -8,7 +8,7 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
jest:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
|
@ -51,8 +51,58 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Test
|
||||
run: pnpm jest-and-coverage
|
||||
- name: Upload Coverage
|
||||
run: pnpm --filter backend test-and-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
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -41,6 +41,7 @@ docker-compose.yml
|
|||
# misskey
|
||||
/build
|
||||
built
|
||||
built-test
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
|
|
|
@ -16,17 +16,22 @@
|
|||
|
||||
### General
|
||||
- Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
|
||||
- Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
|
||||
|
||||
### Client
|
||||
- Feat: 新しいゲームを追加
|
||||
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
|
||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
|
||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||
- Enhance: チャンネルノートのピン留めをノートのメニューからできるよ
|
||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||
- Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||
- Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916)
|
||||
- Enhance: クリップをエクスポートできるように
|
||||
- Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正
|
||||
|
||||
## 2023.12.2
|
||||
|
||||
|
|
|
@ -1192,6 +1192,7 @@ export interface Locale {
|
|||
"decorate": string;
|
||||
"addMfmFunction": string;
|
||||
"enableQuickAddMfmFunction": string;
|
||||
"bubbleGame": string;
|
||||
"_announcement": {
|
||||
"forExistingUsers": string;
|
||||
"forExistingUsersDescription": string;
|
||||
|
@ -1656,6 +1657,15 @@ export interface Locale {
|
|||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
"_bubbleGameDoubleExplodingHead": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
"flavor": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
"_role": {
|
||||
|
@ -2256,6 +2266,7 @@ export interface Locale {
|
|||
"_exportOrImport": {
|
||||
"allNotes": string;
|
||||
"favoritedNotes": string;
|
||||
"clips": string;
|
||||
"followingList": string;
|
||||
"muteList": string;
|
||||
"blockingList": string;
|
||||
|
|
|
@ -1189,6 +1189,7 @@ seasonalScreenEffect: "季節に応じた画面の演出"
|
|||
decorate: "デコる"
|
||||
addMfmFunction: "装飾を追加"
|
||||
enableQuickAddMfmFunction: "高度なMFMのピッカーを表示する"
|
||||
bubbleGame: "バブルゲーム"
|
||||
|
||||
_announcement:
|
||||
forExistingUsers: "既存ユーザーのみ"
|
||||
|
@ -1567,6 +1568,13 @@ _achievements:
|
|||
_tutorialCompleted:
|
||||
title: "Misskey初心者講座 修了証"
|
||||
description: "チュートリアルを完了した"
|
||||
_bubbleGameExplodingHead:
|
||||
title: "🤯"
|
||||
description: "バブルゲームで最も大きいモノを出した"
|
||||
_bubbleGameDoubleExplodingHead:
|
||||
title: "ダブル🤯"
|
||||
description: "バブルゲームで最も大きいモノを2つ同時に出した"
|
||||
flavor: "これくらいの おべんとばこに 🤯 🤯 ちょっとつめて"
|
||||
|
||||
_role:
|
||||
new: "ロールの作成"
|
||||
|
@ -2159,6 +2167,7 @@ _profile:
|
|||
_exportOrImport:
|
||||
allNotes: "全てのノート"
|
||||
favoritedNotes: "お気に入りにしたノート"
|
||||
clips: "クリップ"
|
||||
followingList: "フォロー"
|
||||
muteList: "ミュート"
|
||||
blockingList: "ブロック"
|
||||
|
|
|
@ -160,7 +160,6 @@ module.exports = {
|
|||
testMatch: [
|
||||
"<rootDir>/test/unit/**/*.ts",
|
||||
"<rootDir>/src/**/*.test.ts",
|
||||
"<rootDir>/test/e2e/**/*.ts",
|
||||
],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
};
|
|
@ -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",
|
||||
],
|
||||
};
|
|
@ -13,6 +13,7 @@
|
|||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
"build": "swc src -d built -D",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc",
|
||||
"watch:swc": "swc src -d built -D -w",
|
||||
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
|
@ -21,11 +22,15 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"eslint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "node ./generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -178,6 +183,7 @@
|
|||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/accepts": "1.3.7",
|
||||
|
@ -226,9 +232,11 @@
|
|||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"execa": "8.0.1",
|
||||
"fkill": "^9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.0.2",
|
||||
"pid-port": "^1.0.0",
|
||||
"simple-oauth2": "5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
|||
import { Config, loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import { allSettled } from './misc/promise-tracker.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const $config: Provider = {
|
||||
|
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
|||
useFactory: (config: Config) => {
|
||||
if (config.meilisearch) {
|
||||
return new MeiliSearch({
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
apiKey: config.meilisearch.apiKey,
|
||||
});
|
||||
} else {
|
||||
|
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential DB queries
|
||||
await allSettled();
|
||||
// And then disconnect from DB
|
||||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
|
|
|
@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
|
|
|
@ -655,7 +655,7 @@ export class DriveService {
|
|||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
||||
|
||||
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
|
||||
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
||||
throw new DriveService.InvalidFileNameError();
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -676,7 +677,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.relayService.deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
|
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
|
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
|
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import type { FilterUnionByProperty } from '@/types.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
|
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async createNotification<T extends MiNotification['type']>(
|
||||
public createNotification<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
notifierId?: MiUser['id'] | null,
|
||||
) {
|
||||
trackPromise(
|
||||
this.#createNotificationInternal(notifieeId, type, data, notifierId),
|
||||
);
|
||||
}
|
||||
|
||||
async #createNotificationInternal<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData } from '../queue/types.js';
|
||||
|
||||
|
@ -106,14 +106,9 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
) {}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential queue jobs
|
||||
await allSettled();
|
||||
// And then close all queues
|
||||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
|
|
|
@ -182,6 +182,16 @@ export class QueueService {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createExportClipsJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportClips', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createExportFavoritesJob(user: ThinUser) {
|
||||
return this.dbQueue.add('exportFavorites', {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
|||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
@ -268,7 +269,7 @@ export class ReactionService {
|
|||
}
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
@ -316,7 +317,7 @@ export class ReactionService {
|
|||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||
}
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ class DeliverManager {
|
|||
}
|
||||
|
||||
// deliver
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export class ServerStatsService implements OnApplicationShutdown {
|
|||
const log = [] as any[];
|
||||
|
||||
ev.on('requestServerStatsLog', x => {
|
||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length ?? 50));
|
||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length));
|
||||
});
|
||||
|
||||
const tick = async () => {
|
||||
|
|
|
@ -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()));
|
||||
}
|
|
@ -24,6 +24,7 @@ import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmo
|
|||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js';
|
||||
import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js';
|
||||
import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js';
|
||||
|
@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||
DeleteDriveFilesProcessorService,
|
||||
ExportCustomEmojisProcessorService,
|
||||
ExportNotesProcessorService,
|
||||
ExportClipsProcessorService,
|
||||
ExportFavoritesProcessorService,
|
||||
ExportFollowingProcessorService,
|
||||
ExportMutingProcessorService,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
|
|||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
import { ExportCustomEmojisProcessorService } from './processors/ExportCustomEmojisProcessorService.js';
|
||||
import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js';
|
||||
import { ExportClipsProcessorService } from './processors/ExportClipsProcessorService.js';
|
||||
import { ExportFollowingProcessorService } from './processors/ExportFollowingProcessorService.js';
|
||||
import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js';
|
||||
import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js';
|
||||
|
@ -91,6 +92,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
|
||||
private exportCustomEmojisProcessorService: ExportCustomEmojisProcessorService,
|
||||
private exportNotesProcessorService: ExportNotesProcessorService,
|
||||
private exportClipsProcessorService: ExportClipsProcessorService,
|
||||
private exportFavoritesProcessorService: ExportFavoritesProcessorService,
|
||||
private exportFollowingProcessorService: ExportFollowingProcessorService,
|
||||
private exportMutingProcessorService: ExportMutingProcessorService,
|
||||
|
@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
case 'deleteDriveFiles': return this.deleteDriveFilesProcessorService.process(job);
|
||||
case 'exportCustomEmojis': return this.exportCustomEmojisProcessorService.process(job);
|
||||
case 'exportNotes': return this.exportNotesProcessorService.process(job);
|
||||
case 'exportClips': return this.exportClipsProcessorService.process(job);
|
||||
case 'exportFavorites': return this.exportFavoritesProcessorService.process(job);
|
||||
case 'exportFollowing': return this.exportFollowingProcessorService.process(job);
|
||||
case 'exportMuting': return this.exportMutingProcessorService.process(job);
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { createTemp } from '@/misc/create-temp.js';
|
||||
import type { MiPoll } from '@/models/Poll.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ExportClipsProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@Inject(DI.clipsRepository)
|
||||
private clipsRepository: ClipsRepository,
|
||||
|
||||
@Inject(DI.clipNotesRepository)
|
||||
private clipNotesRepository: ClipNotesRepository,
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||
this.logger.info(`Exporting clips of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
||||
this.logger.info(`Temp file is ${path}`);
|
||||
|
||||
try {
|
||||
const stream = Writable.toWeb(fs.createWriteStream(path, { flags: 'a' }));
|
||||
const writer = stream.getWriter();
|
||||
writer.closed.catch(this.logger.error);
|
||||
|
||||
await writer.write('[');
|
||||
|
||||
await this.processClips(writer, user, job);
|
||||
|
||||
await writer.write(']');
|
||||
await writer.close();
|
||||
|
||||
this.logger.succ(`Exported to: ${path}`);
|
||||
|
||||
const fileName = 'clips-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
|
||||
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
|
||||
|
||||
this.logger.succ(`Exported to: ${driveFile.id}`);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async processClips(writer: WritableStreamDefaultWriter, user: MiUser, job: Bull.Job<DbJobDataWithUser>) {
|
||||
let exportedClipsCount = 0;
|
||||
let cursor: MiClip['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const clips = await this.clipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (clips.length === 0) {
|
||||
job.updateProgress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = clips.at(-1)?.id ?? null;
|
||||
|
||||
for (const clip of clips) {
|
||||
// Stringify but remove the last `]}`
|
||||
const content = JSON.stringify(this.serializeClip(clip)).slice(0, -2);
|
||||
const isFirst = exportedClipsCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
await this.processClipNotes(writer, clip.id);
|
||||
|
||||
await writer.write(']}');
|
||||
exportedClipsCount++;
|
||||
}
|
||||
|
||||
const total = await this.clipsRepository.countBy({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
job.updateProgress(exportedClipsCount / total);
|
||||
}
|
||||
}
|
||||
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
|
||||
let exportedClipNotesCount = 0;
|
||||
let cursor: MiClipNote['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const clipNotes = await this.clipNotesRepository.find({
|
||||
where: {
|
||||
clipId,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ['note', 'note.user'],
|
||||
}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (clipNotes.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = clipNotes.at(-1)?.id ?? null;
|
||||
|
||||
for (const clipNote of clipNotes) {
|
||||
let poll: MiPoll | undefined;
|
||||
if (clipNote.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
|
||||
}
|
||||
const content = JSON.stringify(this.serializeClipNote(clipNote, poll));
|
||||
const isFirst = exportedClipNotesCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
exportedClipNotesCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private serializeClip(clip: MiClip): Record<string, unknown> {
|
||||
return {
|
||||
id: clip.id,
|
||||
name: clip.name,
|
||||
description: clip.description,
|
||||
lastClippedAt: clip.lastClippedAt?.toISOString(),
|
||||
clipNotes: [],
|
||||
};
|
||||
}
|
||||
|
||||
private serializeClipNote(clip: MiClipNote & { note: MiNote & { user: MiUser } }, poll: MiPoll | undefined): Record<string, unknown> {
|
||||
return {
|
||||
id: clip.id,
|
||||
createdAt: this.idService.parse(clip.id).date.toISOString(),
|
||||
note: {
|
||||
id: clip.note.id,
|
||||
text: clip.note.text,
|
||||
createdAt: this.idService.parse(clip.note.id).date.toISOString(),
|
||||
fileIds: clip.note.fileIds,
|
||||
replyId: clip.note.replyId,
|
||||
renoteId: clip.note.renoteId,
|
||||
poll: poll,
|
||||
cw: clip.note.cw,
|
||||
visibility: clip.note.visibility,
|
||||
visibleUserIds: clip.note.visibleUserIds,
|
||||
localOnly: clip.note.localOnly,
|
||||
reactionAcceptance: clip.note.reactionAcceptance,
|
||||
uri: clip.note.uri,
|
||||
url: clip.note.url,
|
||||
user: {
|
||||
id: clip.note.user.id,
|
||||
name: clip.note.user.name,
|
||||
username: clip.note.user.username,
|
||||
host: clip.note.user.host,
|
||||
uri: clip.note.user.uri,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -208,6 +208,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
|||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
|
@ -569,6 +570,7 @@ const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass:
|
|||
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
|
||||
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
|
||||
const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default };
|
||||
const $i_exportClips: Provider = { provide: 'ep:i/export-clips', useClass: ep___i_exportClips.default };
|
||||
const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default };
|
||||
const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default };
|
||||
const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default };
|
||||
|
@ -934,6 +936,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
|
@ -1293,6 +1296,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
$i_exportNotes,
|
||||
$i_exportClips,
|
||||
$i_exportFavorites,
|
||||
$i_exportUserLists,
|
||||
$i_exportAntennas,
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { permissions } from 'misskey-js';
|
||||
import type { Schema } from '@/misc/json-schema.js';
|
||||
import { RolePolicies } from '@/core/RoleService.js';
|
||||
|
||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||
|
@ -209,6 +209,7 @@ import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
|||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
|
||||
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
|
||||
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
|
||||
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
|
||||
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
|
||||
|
@ -568,6 +569,7 @@ const eps = [
|
|||
['i/export-following', ep___i_exportFollowing],
|
||||
['i/export-mute', ep___i_exportMute],
|
||||
['i/export-notes', ep___i_exportNotes],
|
||||
['i/export-clips', ep___i_exportClips],
|
||||
['i/export-favorites', ep___i_exportFavorites],
|
||||
['i/export-user-lists', ep___i_exportUserLists],
|
||||
['i/export-antennas', ep___i_exportAntennas],
|
||||
|
|
|
@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
antenna.isActive = true;
|
||||
antenna.lastUsedAt = new Date();
|
||||
this.antennasRepository.update(antenna.id, antenna);
|
||||
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||
|
||||
if (needPublishEvent) {
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
|
||||
export const meta = {
|
||||
secure: true,
|
||||
requireCredential: true,
|
||||
limit: {
|
||||
duration: ms('1day'),
|
||||
max: 1,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
this.queueService.createExportClipsJob(me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ class UserListChannel extends Channel {
|
|||
private membershipsMap: Record<string, Pick<MiUserListMembership, 'withReplies'> | undefined> = {};
|
||||
private listUsersClock: NodeJS.Timeout;
|
||||
private withFiles: boolean;
|
||||
private withRenotes: boolean;
|
||||
|
||||
constructor(
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
@ -39,6 +40,7 @@ class UserListChannel extends Channel {
|
|||
public async init(params: any) {
|
||||
this.listId = params.listId as string;
|
||||
this.withFiles = params.withFiles ?? false;
|
||||
this.withRenotes = params.withRenotes ?? true;
|
||||
|
||||
// Check existence and owner
|
||||
const listExist = await this.userListsRepository.exist({
|
||||
|
@ -104,6 +106,8 @@ class UserListChannel extends Channel {
|
|||
}
|
||||
}
|
||||
|
||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
|
|
|
@ -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.'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
|
@ -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
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmitOnError": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"target": "ES2022",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"rootDir": "../src",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
},
|
||||
"outDir": "../built-test",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"../src/@types",
|
||||
"../node_modules/@types",
|
||||
"../node_modules"
|
||||
],
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"../src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"../src/**/*.test.ts"
|
||||
]
|
||||
}
|
|
@ -10,7 +10,7 @@ import * as crypto from 'node:crypto';
|
|||
import cbor from 'cbor';
|
||||
import * as OTPAuth from 'otpauth';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { api, signup, startServer } from '../utils.js';
|
||||
import { api, signup } from '../utils.js';
|
||||
import type {
|
||||
AuthenticationResponseJSON,
|
||||
AuthenticatorAssertionResponseJSON,
|
||||
|
@ -19,11 +19,9 @@ import type {
|
|||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/typescript-types';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('2要素認証', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
|
||||
const config = loadConfig();
|
||||
|
@ -185,14 +183,9 @@ describe('2要素認証', () => {
|
|||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username, password });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('が設定でき、OTPでログインできる。', async () => {
|
||||
const registerResponse = await api('/i/2fa/register', {
|
||||
password,
|
||||
|
|
|
@ -6,24 +6,20 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { inspect } from 'node:util';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import {
|
||||
signup,
|
||||
post,
|
||||
userList,
|
||||
page,
|
||||
role,
|
||||
startServer,
|
||||
api,
|
||||
successfulApiCall,
|
||||
failedApiCall,
|
||||
uploadFile,
|
||||
post,
|
||||
role,
|
||||
signup,
|
||||
successfulApiCall,
|
||||
testPaginationConsistency,
|
||||
uploadFile,
|
||||
userList,
|
||||
} from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
const compareBy = <T extends { id: string }>(selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
|
||||
return selector(a).localeCompare(selector(b));
|
||||
|
@ -54,8 +50,6 @@ describe('アンテナ', () => {
|
|||
withReplies: false,
|
||||
};
|
||||
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let root: User;
|
||||
let alice: User;
|
||||
let bob: User;
|
||||
|
@ -79,10 +73,6 @@ describe('アンテナ', () => {
|
|||
let userMutingAlice: User;
|
||||
let userMutedByAlice: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await signup({ username: 'root' });
|
||||
alice = await signup({ username: 'alice' });
|
||||
|
@ -136,10 +126,6 @@ describe('アンテナ', () => {
|
|||
await api('mute/create', { userId: userMutedByAlice.id }, alice);
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// テスト間で影響し合わないように毎回全部消す。
|
||||
for (const user of [alice, bob]) {
|
||||
|
|
|
@ -6,21 +6,10 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('API visibility', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Note visibility', () => {
|
||||
//#region vars
|
||||
/** ヒロイン */
|
||||
|
|
|
@ -7,27 +7,30 @@ process.env.NODE_ENV = 'test';
|
|||
|
||||
import * as assert from 'assert';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import {
|
||||
api,
|
||||
connectStream,
|
||||
createAppToken,
|
||||
failedApiCall,
|
||||
relativeFetch,
|
||||
signup,
|
||||
successfulApiCall,
|
||||
uploadFile,
|
||||
waitFire,
|
||||
} from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('API', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('General validation', () => {
|
||||
test('wrong type', async () => {
|
||||
const res = await api('/test', {
|
||||
|
|
|
@ -6,29 +6,21 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Block', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
// alice blocks bob
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('Block作成', async () => {
|
||||
const res = await api('/blocking/create', {
|
||||
userId: bob.id,
|
||||
|
|
|
@ -18,25 +18,13 @@ import { paramDef as UnfavoriteParamDef } from '@/server/api/endpoints/clips/unf
|
|||
import { paramDef as AddNoteParamDef } from '@/server/api/endpoints/clips/add-note.js';
|
||||
import { paramDef as RemoveNoteParamDef } from '@/server/api/endpoints/clips/remove-note.js';
|
||||
import { paramDef as NotesParamDef } from '@/server/api/endpoints/clips/notes.js';
|
||||
import {
|
||||
signup,
|
||||
post,
|
||||
startServer,
|
||||
api,
|
||||
successfulApiCall,
|
||||
failedApiCall,
|
||||
ApiRequest,
|
||||
hiddenNote,
|
||||
} from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, ApiRequest, failedApiCall, hiddenNote, post, signup, successfulApiCall } from '../utils.js';
|
||||
|
||||
describe('クリップ', () => {
|
||||
type User = Packed<'User'>;
|
||||
type Note = Packed<'Note'>;
|
||||
type Clip = Packed<'Clip'>;
|
||||
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: User;
|
||||
let bob: User;
|
||||
let aliceNote: Note;
|
||||
|
@ -145,7 +133,6 @@ describe('クリップ', () => {
|
|||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
|
||||
|
@ -160,10 +147,6 @@ describe('クリップ', () => {
|
|||
bobSpecifiedNote = await post(bob, { text: 'specified only', visibility: 'specified' }) as any;
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// テスト間で影響し合わないように毎回全部消す。
|
||||
for (const user of [alice, bob]) {
|
||||
|
|
|
@ -10,30 +10,22 @@ import * as assert from 'assert';
|
|||
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||
import { Blob } from 'node-fetch';
|
||||
import { MiUser } from '@/models/_.js';
|
||||
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Endpoints', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
let dave: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
dave = await signup({ username: 'dave' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('signup', () => {
|
||||
test('不正なユーザー名でアカウントが作成できない', async () => {
|
||||
const res = await api('signup', {
|
||||
|
@ -710,6 +702,18 @@ describe('Endpoints', () => {
|
|||
assert.strictEqual(res.status, 400);
|
||||
});
|
||||
|
||||
test('不正なファイル名で怒られる', async () => {
|
||||
const file = (await uploadFile(alice)).body;
|
||||
const newName = '';
|
||||
|
||||
const res = await api('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: newName,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
});
|
||||
|
||||
test('間違ったIDで怒られる', async () => {
|
||||
const res = await api('/drive/files/update', {
|
||||
fileId: 'kyoppie',
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { api, port, post, signup, startJobQueue } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('export-clips', () => {
|
||||
let queue: INestApplicationContext;
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
// XXX: Any better way to get the result?
|
||||
async function pollFirstDriveFile() {
|
||||
while (true) {
|
||||
const files = (await api('/drive/files', {}, alice)).body;
|
||||
if (!files.length) {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
continue;
|
||||
}
|
||||
if (files.length > 1) {
|
||||
throw new Error('Too many files?');
|
||||
}
|
||||
const file = (await api('/drive/files/show', { fileId: files[0].id }, alice)).body;
|
||||
const res = await fetch(new URL(new URL(file.url).pathname, `http://127.0.0.1:${port}`));
|
||||
return await res.json();
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
queue = await startJobQueue();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await queue.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean all clips and files of alice
|
||||
const clips = (await api('/clips/list', {}, alice)).body;
|
||||
for (const clip of clips) {
|
||||
const res = await api('/clips/delete', { clipId: clip.id }, alice);
|
||||
if (res.status !== 204) {
|
||||
throw new Error('Failed to delete clip');
|
||||
}
|
||||
}
|
||||
const files = (await api('/drive/files', {}, alice)).body;
|
||||
for (const file of files) {
|
||||
const res = await api('/drive/files/delete', { fileId: file.id }, alice);
|
||||
if (res.status !== 204) {
|
||||
throw new Error('Failed to delete file');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('basic export', async () => {
|
||||
let res = await api('/clips/create', {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
||||
res = await api('/i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'foo');
|
||||
assert.strictEqual(exported[0].description, 'bar');
|
||||
assert.strictEqual(exported[0].clipNotes.length, 0);
|
||||
});
|
||||
|
||||
test('export with notes', async () => {
|
||||
let res = await api('/clips/create', {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip = res.body;
|
||||
|
||||
const note1 = await post(alice, {
|
||||
text: 'baz1',
|
||||
});
|
||||
|
||||
const note2 = await post(alice, {
|
||||
text: 'baz2',
|
||||
poll: {
|
||||
choices: ['sakura', 'izumi', 'ako'],
|
||||
},
|
||||
});
|
||||
|
||||
for (const note of [note1, note2]) {
|
||||
res = await api('/clips/add-note', {
|
||||
clipId: clip.id,
|
||||
noteId: note.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
}
|
||||
|
||||
res = await api('/i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'foo');
|
||||
assert.strictEqual(exported[0].description, 'bar');
|
||||
assert.strictEqual(exported[0].clipNotes.length, 2);
|
||||
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
|
||||
assert.strictEqual(exported[0].clipNotes[1].note.text, 'baz2');
|
||||
assert.deepStrictEqual(exported[0].clipNotes[1].note.poll.choices[0], 'sakura');
|
||||
});
|
||||
|
||||
test('multiple clips', async () => {
|
||||
let res = await api('/clips/create', {
|
||||
name: 'kawaii',
|
||||
description: 'kawaii',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip1 = res.body;
|
||||
|
||||
res = await api('/clips/create', {
|
||||
name: 'yuri',
|
||||
description: 'yuri',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip2 = res.body;
|
||||
|
||||
const note1 = await post(alice, {
|
||||
text: 'baz1',
|
||||
});
|
||||
|
||||
const note2 = await post(alice, {
|
||||
text: 'baz2',
|
||||
});
|
||||
|
||||
res = await api('/clips/add-note', {
|
||||
clipId: clip1.id,
|
||||
noteId: note1.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
res = await api('/clips/add-note', {
|
||||
clipId: clip2.id,
|
||||
noteId: note2.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
res = await api('/i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'kawaii');
|
||||
assert.strictEqual(exported[0].clipNotes.length, 1);
|
||||
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz1');
|
||||
assert.strictEqual(exported[1].name, 'yuri');
|
||||
assert.strictEqual(exported[1].clipNotes.length, 1);
|
||||
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
|
||||
});
|
||||
|
||||
test('Clipping other user\'s note', async () => {
|
||||
let res = await api('/clips/create', {
|
||||
name: 'kawaii',
|
||||
description: 'kawaii',
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const clip = res.body;
|
||||
|
||||
const note = await post(bob, {
|
||||
text: 'baz',
|
||||
visibility: 'followers',
|
||||
});
|
||||
|
||||
res = await api('/clips/add-note', {
|
||||
clipId: clip.id,
|
||||
noteId: note.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
res = await api('/i/export-clips', {}, alice);
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].name, 'kawaii');
|
||||
assert.strictEqual(exported[0].clipNotes.length, 1);
|
||||
assert.strictEqual(exported[0].clipNotes[0].note.text, 'baz');
|
||||
assert.strictEqual(exported[0].clipNotes[0].note.user.username, 'bob');
|
||||
});
|
||||
});
|
|
@ -6,9 +6,8 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { startServer, channel, clip, cookie, galleryPost, signup, page, play, post, simpleGet, uploadFile } from '../utils.js';
|
||||
import { channel, clip, cookie, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
|
||||
import type { SimpleGetResponse } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
// Request Accept
|
||||
|
@ -23,8 +22,6 @@ const HTML = 'text/html; charset=utf-8';
|
|||
const JSON_UTF8 = 'application/json; charset=utf-8';
|
||||
|
||||
describe('Webリソース', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let aliceUploadedFile: any;
|
||||
let alicesPost: any;
|
||||
|
@ -79,7 +76,6 @@ describe('Webリソース', () => {
|
|||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
aliceUploadedFile = await uploadFile(alice);
|
||||
alicesPost = await post(alice, {
|
||||
|
@ -96,10 +92,6 @@ describe('Webリソース', () => {
|
|||
bob = await signup({ username: 'bob' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{ path: '/', type: HTML },
|
||||
{ path: '/docs/ja-JP/about', type: HTML }, // "指定されたURLに該当するページはありませんでした。"
|
||||
|
|
|
@ -6,26 +6,18 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, startServer, simpleGet } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, signup, simpleGet } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('FF visibility', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
|
||||
await api('/i/update', {
|
||||
followingVisibility: 'public',
|
||||
|
|
|
@ -3,19 +3,19 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { loadConfig } from '@/config.js';
|
||||
import { MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { jobQueue } from '@/boot/common.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { jobQueue } from '@/boot/common.js';
|
||||
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Account Move', () => {
|
||||
let app: INestApplicationContext;
|
||||
let jq: INestApplicationContext;
|
||||
let url: URL;
|
||||
|
||||
|
@ -30,8 +30,8 @@ describe('Account Move', () => {
|
|||
let Users: UsersRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
jq = await jobQueue();
|
||||
|
||||
const config = loadConfig();
|
||||
url = new URL(config.url);
|
||||
const connection = await initTestDb(false);
|
||||
|
@ -46,7 +46,7 @@ describe('Account Move', () => {
|
|||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await Promise.all([app.close(), jq.close()]);
|
||||
await jq.close();
|
||||
});
|
||||
|
||||
describe('Create Alias', () => {
|
||||
|
|
|
@ -6,29 +6,21 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, react, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Mute', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('ミュート作成', async () => {
|
||||
const res = await api('/mute/create', {
|
||||
userId: carol.id,
|
||||
|
|
|
@ -6,20 +6,9 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { relativeFetch, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { relativeFetch } from '../utils.js';
|
||||
|
||||
describe('nodeinfo', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('nodeinfo 2.1', async () => {
|
||||
const res = await relativeFetch('nodeinfo/2.1');
|
||||
assert.ok(res.ok);
|
||||
|
|
|
@ -8,29 +8,22 @@ process.env.NODE_ENV = 'test';
|
|||
import * as assert from 'assert';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Note', () => {
|
||||
let app: INestApplicationContext;
|
||||
let Notes: any;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Notes = connection.getRepository(MiNote);
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('投稿できる', async () => {
|
||||
const post = {
|
||||
text: 'test',
|
||||
|
|
|
@ -11,13 +11,18 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig, ClientCredentials, ModuleOptions } from 'simple-oauth2';
|
||||
import {
|
||||
AuthorizationCode,
|
||||
type AuthorizationTokenConfig,
|
||||
ClientCredentials,
|
||||
ModuleOptions,
|
||||
ResourceOwnerPassword,
|
||||
} from 'simple-oauth2';
|
||||
import pkceChallenge from 'pkce-challenge';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify';
|
||||
import { api, port, signup, startServer } from '../utils.js';
|
||||
import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
|
||||
import { api, port, sendEnvUpdateRequest, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
const host = `http://127.0.0.1:${port}`;
|
||||
|
||||
|
@ -147,7 +152,6 @@ async function assertDirectError(response: Response, status: number, error: stri
|
|||
}
|
||||
|
||||
describe('OAuth', () => {
|
||||
let app: INestApplicationContext;
|
||||
let fastify: FastifyInstance;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
|
@ -156,7 +160,6 @@ describe('OAuth', () => {
|
|||
let sender: (reply: FastifyReply) => void;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
|
||||
|
@ -168,7 +171,7 @@ describe('OAuth', () => {
|
|||
}, 1000 * 60 * 2);
|
||||
|
||||
beforeEach(async () => {
|
||||
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '';
|
||||
await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '' });
|
||||
sender = (reply): void => {
|
||||
reply.send(`
|
||||
<!DOCTYPE html>
|
||||
|
@ -180,7 +183,6 @@ describe('OAuth', () => {
|
|||
|
||||
afterAll(async () => {
|
||||
await fastify.close();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('Full flow', async () => {
|
||||
|
@ -881,7 +883,7 @@ describe('OAuth', () => {
|
|||
});
|
||||
|
||||
test('Disallow loopback', async () => {
|
||||
process.env.MISSKEY_TEST_CHECK_IP_RANGE = '1';
|
||||
await sendEnvUpdateRequest({ key: 'MISSKEY_TEST_CHECK_IP_RANGE', value: '1' });
|
||||
|
||||
const client = new AuthorizationCode(clientConfig);
|
||||
const response = await fetch(client.authorizeURL({
|
||||
|
|
|
@ -6,29 +6,21 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, react, startServer, waitFire, sleep } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, signup, sleep, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Renote Mute', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('ミュート作成', async () => {
|
||||
const res = await api('/renote-mute/create', {
|
||||
userId: carol.id,
|
||||
|
|
|
@ -8,12 +8,10 @@ process.env.NODE_ENV = 'test';
|
|||
import * as assert from 'assert';
|
||||
import { WebSocket } from 'ws';
|
||||
import { MiFollowing } from '@/models/Following.js';
|
||||
import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, createAppToken, initTestDb, port, post, signup, waitFire } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Streaming', () => {
|
||||
let app: INestApplicationContext;
|
||||
let Followings: any;
|
||||
|
||||
const follow = async (follower: any, followee: any) => {
|
||||
|
@ -48,7 +46,6 @@ describe('Streaming', () => {
|
|||
let list: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Followings = connection.getRepository(MiFollowing);
|
||||
|
||||
|
@ -95,10 +92,6 @@ describe('Streaming', () => {
|
|||
}, chitose);
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Events', () => {
|
||||
test('mention event', async () => {
|
||||
const fired = await waitFire(
|
||||
|
|
|
@ -6,28 +6,20 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, connectStream, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, connectStream, post, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('Note thread mute', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let bob: misskey.entities.SignupResponse;
|
||||
let carol: misskey.entities.SignupResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('notes/mentions にミュートしているスレッドの投稿が含まれない', async () => {
|
||||
const bobNote = await post(bob, { text: '@alice @carol root note' });
|
||||
const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' });
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
// How to run:
|
||||
// pnpm jest -- e2e/timelines.ts
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js';
|
||||
|
||||
function genHost() {
|
||||
return randomString() + '.example.com';
|
||||
|
@ -21,16 +17,6 @@ function waitForPushToTl() {
|
|||
return sleep(500);
|
||||
}
|
||||
|
||||
let app: INestApplicationContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('Timelines', () => {
|
||||
describe('Home TL', () => {
|
||||
test.concurrent('自分の visibility: followers なノートが含まれる', async () => {
|
||||
|
@ -334,8 +320,9 @@ describe('Timelines', () => {
|
|||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||
|
||||
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
|
||||
await api('/following/create', { userId: bob.id }, alice);
|
||||
await sleep(1000);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
@ -348,8 +335,9 @@ describe('Timelines', () => {
|
|||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||
|
||||
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
|
||||
await api('/following/create', { userId: bob.id }, alice);
|
||||
await sleep(1000);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
@ -762,8 +750,9 @@ describe('Timelines', () => {
|
|||
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||
|
||||
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
|
||||
await api('/following/create', { userId: bob.id }, alice);
|
||||
await sleep(1000);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
@ -776,8 +765,9 @@ describe('Timelines', () => {
|
|||
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
|
||||
|
||||
await sendEnvUpdateRequest({ key: 'FORCE_FOLLOW_REMOTE_USER_FOR_TESTING', value: 'true' });
|
||||
await api('/following/create', { userId: bob.id }, alice);
|
||||
await sleep(1000);
|
||||
|
||||
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
|
|
@ -6,20 +6,16 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { signup, api, post, uploadUrl, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { api, post, signup, uploadUrl } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('users/notes', () => {
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let alice: misskey.entities.SignupResponse;
|
||||
let jpgNote: any;
|
||||
let pngNote: any;
|
||||
let jpgPngNote: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
||||
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
|
||||
|
@ -34,10 +30,6 @@ describe('users/notes', () => {
|
|||
});
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async() => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('withFiles', async () => {
|
||||
const res = await api('/users/notes', {
|
||||
userId: alice.id,
|
||||
|
|
|
@ -8,20 +8,8 @@ process.env.NODE_ENV = 'test';
|
|||
import * as assert from 'assert';
|
||||
import { inspect } from 'node:util';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import {
|
||||
signup,
|
||||
post,
|
||||
page,
|
||||
role,
|
||||
startServer,
|
||||
api,
|
||||
successfulApiCall,
|
||||
failedApiCall,
|
||||
uploadFile,
|
||||
} from '../utils.js';
|
||||
import { api, page, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
|
||||
describe('ユーザー', () => {
|
||||
// エンティティとしてのユーザーを主眼においたテストを記述する
|
||||
|
@ -185,8 +173,6 @@ describe('ユーザー', () => {
|
|||
});
|
||||
};
|
||||
|
||||
let app: INestApplicationContext;
|
||||
|
||||
let root: User;
|
||||
let alice: User;
|
||||
let aliceNote: misskey.entities.Note;
|
||||
|
@ -230,10 +216,6 @@ describe('ユーザー', () => {
|
|||
let userFollowRequesting: User;
|
||||
let userFollowRequested: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
beforeAll(async () => {
|
||||
root = await signup({ username: 'root' });
|
||||
alice = await signup({ username: 'alice' });
|
||||
|
@ -321,10 +303,6 @@ describe('ユーザー', () => {
|
|||
await api('following/create', { userId: userFollowRequested.id }, userFollowRequesting);
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
alice = {
|
||||
...alice,
|
||||
|
|
|
@ -6,24 +6,16 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { host, origin, relativeFetch, signup, startServer } from '../utils.js';
|
||||
import type { INestApplicationContext } from '@nestjs/common';
|
||||
import { host, origin, relativeFetch, signup } from '../utils.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('.well-known', () => {
|
||||
let app: INestApplicationContext;
|
||||
let alice: misskey.entities.User;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await startServer();
|
||||
|
||||
alice = await signup({ username: 'alice' });
|
||||
}, 1000 * 60 * 2);
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
test('nodeinfo', async () => {
|
||||
const res = await relativeFetch('.well-known/nodeinfo');
|
||||
assert.ok(res.ok);
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import { initTestDb, sendEnvResetRequest } from './utils.js';
|
||||
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
initTestDb(false),
|
||||
sendEnvResetRequest(),
|
||||
]);
|
||||
});
|
|
@ -15,7 +15,13 @@ import type { LoggerService } from '@/core/LoggerService.js';
|
|||
import type { MetaService } from '@/core/MetaService.js';
|
||||
import type { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { NoteReactionsRepository, NotesRepository, PollsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import type {
|
||||
FollowRequestsRepository,
|
||||
NoteReactionsRepository,
|
||||
NotesRepository,
|
||||
PollsRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
|
||||
type MockResponse = {
|
||||
type: string;
|
||||
|
|
|
@ -10,7 +10,13 @@ import { ModuleMocker } from 'jest-mock';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/_.js';
|
||||
import type {
|
||||
AnnouncementReadsRepository,
|
||||
AnnouncementsRepository,
|
||||
MiAnnouncement,
|
||||
MiUser,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DeleteObjectCommandOutput, DeleteObjectCommand, NoSuchKey, InvalidObjectState, S3Client } from '@aws-sdk/client-s3';
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
DeleteObjectCommandOutput,
|
||||
InvalidObjectState,
|
||||
NoSuchKey,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { mockClient } from 'aws-sdk-client-mock';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
|
|
|
@ -55,7 +55,8 @@ describe('FetchInstanceMetadataService', () => {
|
|||
return { fetch: jest.fn() };
|
||||
} else if (token === DI.redis) {
|
||||
return mockRedis;
|
||||
}})
|
||||
}
|
||||
})
|
||||
.compile();
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
|
|
@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { dirname } from 'node:path';
|
||||
import { ModuleMocker } from 'jest-mock';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { describe, beforeAll, afterAll, test } from '@jest/globals';
|
||||
import { afterAll, beforeAll, describe, test } from '@jest/globals';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { FileInfoService } from '@/core/FileInfoService.js';
|
||||
//import { DI } from '@/di-symbols.js';
|
||||
|
|
|
@ -6,15 +6,13 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import { ModuleMocker } from 'jest-mock';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import type { MetasRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
||||
describe('MetaService', () => {
|
||||
let app: TestingModule;
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Test } from '@nestjs/testing';
|
|||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/_.js';
|
||||
import type { MiRole, MiUser, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { UploadPartCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import {
|
||||
CompleteMultipartUploadCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
UploadPartCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { mockClient } from 'aws-sdk-client-mock';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
*/
|
||||
|
||||
import { ulid } from 'ulid';
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { describe, expect, test } from '@jest/globals';
|
||||
import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js';
|
||||
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
|
||||
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
|
||||
import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js';
|
||||
import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js';
|
||||
import { parseUlid, ulidRegExp } from '@/misc/id/ulid.js';
|
||||
|
||||
describe('misc:id', () => {
|
||||
test('aid', () => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { describe, expect, test } from '@jest/globals';
|
||||
import { contentDisposition } from '@/misc/content-disposition.js';
|
||||
|
||||
describe('misc:content-disposition', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import * as assert from 'node:assert';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { isAbsolute, basename } from 'node:path';
|
||||
import { basename, isAbsolute } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { inspect } from 'node:util';
|
||||
import WebSocket, { ClientOptions } from 'ws';
|
||||
|
@ -17,7 +17,7 @@ import { entities } from '../src/postgres.js';
|
|||
import { loadConfig } from '../src/config.js';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
export { server as startServer } from '@/boot/common.js';
|
||||
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
|
||||
|
||||
interface UserToken {
|
||||
token: string;
|
||||
|
@ -68,7 +68,11 @@ export const failedApiCall = async <T, >(request: ApiRequest, assertion: {
|
|||
return res.body;
|
||||
};
|
||||
|
||||
const request = async (path: string, params: any, me?: UserToken): Promise<{ status: number, headers: Headers, body: any }> => {
|
||||
const request = async (path: string, params: any, me?: UserToken): Promise<{
|
||||
status: number,
|
||||
headers: Headers,
|
||||
body: any
|
||||
}> => {
|
||||
const bodyAuth: Record<string, string> = {};
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -275,7 +279,11 @@ interface UploadOptions {
|
|||
* Upload file
|
||||
* @param user User
|
||||
*/
|
||||
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{ status: number, headers: Headers, body: misskey.Endpoints['drive/files/create']['res'] | null }> => {
|
||||
export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadOptions = {}): Promise<{
|
||||
status: number,
|
||||
headers: Headers,
|
||||
body: misskey.Endpoints['drive/files/create']['res'] | null
|
||||
}> => {
|
||||
const absPath = path == null
|
||||
? new URL('resources/Lenna.jpg', import.meta.url)
|
||||
: isAbsolute(path.toString())
|
||||
|
@ -557,3 +565,34 @@ export function sleep(msec: number) {
|
|||
}, msec);
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
|
||||
const res = await fetch(
|
||||
`http://localhost:${port + 1000}/env`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('server env update failed.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEnvResetRequest() {
|
||||
const res = await fetch(
|
||||
`http://localhost:${port + 1000}/env-reset`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('server env update failed.');
|
||||
}
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 248 KiB |
|
@ -4,7 +4,7 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"watch": "vite",
|
||||
"dev": "vite --config vite.config.local-dev.ts",
|
||||
"dev": "vite --config vite.config.local-dev.ts --debug hmr",
|
||||
"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\"",
|
||||
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||
|
|
|
@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
|||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { setupRouter } from '@/global/router/definition.js';
|
||||
|
||||
export async function common(createVue: () => App<Element>) {
|
||||
console.info(`Misskey v${version}`);
|
||||
|
@ -241,6 +242,8 @@ export async function common(createVue: () => App<Element>) {
|
|||
|
||||
const app = createVue();
|
||||
|
||||
setupRouter(app);
|
||||
|
||||
if (_DEV_) {
|
||||
app.config.performance = true;
|
||||
}
|
||||
|
|
|
@ -3,23 +3,23 @@
|
|||
* 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 { ui } from '@/config.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 * as sound from '@/scripts/sound.js';
|
||||
import { $i, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { $i, signout, updateAccount } from '@/account.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
export async function mainBoot() {
|
||||
const { isClientUpdated } = await common(() => createApp(
|
||||
|
|
|
@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js';
|
|||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
@ -23,26 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<div ref="contents" :class="$style.root" style="container-type: inline-size;">
|
||||
<RouterView :key="reloadCount" :router="router"/>
|
||||
<RouterView :key="reloadCount" :router="windowRouter"/>
|
||||
</div>
|
||||
</MkWindow>
|
||||
</template>
|
||||
|
||||
<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 MkWindow from '@/components/MkWindow.vue';
|
||||
import { popout as _popout } from '@/scripts/popout.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { mainRouter, routes, page } from '@/router.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { Router, useScrollPositionManager } from '@/nirax.js';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
import { openingWindowsCount } from '@/os.js';
|
||||
import { claimAchievement } from '@/scripts/achievements.js';
|
||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||
import { useRouterFactory } from '@/global/router/supplier.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
const props = defineProps<{
|
||||
initialPath: string;
|
||||
|
@ -52,14 +52,15 @@ defineEmits<{
|
|||
(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 pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||
const history = ref<{ path: string; key: any; }[]>([{
|
||||
path: router.getCurrentPath(),
|
||||
key: router.getCurrentKey(),
|
||||
path: windowRouter.getCurrentPath(),
|
||||
key: windowRouter.getCurrentKey(),
|
||||
}]);
|
||||
const buttonsLeft = computed(() => {
|
||||
const buttons = [];
|
||||
|
@ -88,11 +89,11 @@ const buttonsRight = computed(() => {
|
|||
});
|
||||
const reloadCount = ref(0);
|
||||
|
||||
router.addListener('push', ctx => {
|
||||
windowRouter.addListener('push', ctx => {
|
||||
history.value.push({ path: ctx.path, key: ctx.key });
|
||||
});
|
||||
|
||||
provide('router', router);
|
||||
provide('router', windowRouter);
|
||||
provideMetadataReceiver((info) => {
|
||||
pageMetadata.value = info;
|
||||
});
|
||||
|
@ -112,20 +113,20 @@ const contextmenu = computed(() => ([{
|
|||
icon: 'ti ti-external-link',
|
||||
text: i18n.ts.openInNewTab,
|
||||
action: () => {
|
||||
window.open(url + router.getCurrentPath(), '_blank', 'noopener');
|
||||
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
|
||||
windowEl.value.close();
|
||||
},
|
||||
}, {
|
||||
icon: 'ti ti-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: () => {
|
||||
copyToClipboard(url + router.getCurrentPath());
|
||||
copyToClipboard(url + windowRouter.getCurrentPath());
|
||||
},
|
||||
}]));
|
||||
|
||||
function back() {
|
||||
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() {
|
||||
|
@ -137,16 +138,16 @@ function close() {
|
|||
}
|
||||
|
||||
function expand() {
|
||||
mainRouter.push(router.getCurrentPath(), 'forcePage');
|
||||
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
|
||||
windowEl.value.close();
|
||||
}
|
||||
|
||||
function popout() {
|
||||
_popout(router.getCurrentPath(), windowEl.value.$el);
|
||||
_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
|
||||
windowEl.value.close();
|
||||
}
|
||||
|
||||
useScrollPositionManager(() => getScrollContainer(contents.value), router);
|
||||
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
||||
|
||||
onMounted(() => {
|
||||
openingWindowsCount.value++;
|
||||
|
|
|
@ -42,6 +42,7 @@ onMounted(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
width: 128px;
|
||||
|
|
|
@ -132,6 +132,7 @@ function connectChannel() {
|
|||
connection.on('mention', onNote);
|
||||
} else if (props.src === 'list') {
|
||||
connection = stream.useChannel('userList', {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
});
|
||||
|
@ -198,6 +199,7 @@ function updatePaginationQuery() {
|
|||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
|
@ -236,8 +238,9 @@ function refreshEndpointAndChannel() {
|
|||
updatePaginationQuery();
|
||||
}
|
||||
|
||||
// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
|
||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel);
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
|
||||
|
||||
// 初回表示用
|
||||
refreshEndpointAndChannel();
|
||||
|
|
|
@ -15,7 +15,7 @@ import * as os from '@/os.js';
|
|||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import { url } from '@/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { useRouter } from '@/global/router/supplier.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
|
|
|
@ -5,15 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
|
||||
<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
|
||||
<span v-else>{{ emoji }}</span>
|
||||
<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject } from 'vue';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import * as os from '@/os.js';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
|
@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null);
|
|||
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
|
||||
|
||||
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
|
||||
const url = computed(() => {
|
||||
return char2path(props.emoji);
|
||||
});
|
||||
const url = computed(() => char2path(props.emoji));
|
||||
const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
|
||||
|
||||
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
|
||||
function computeTitle(event: PointerEvent): void {
|
||||
|
|
|
@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
|
||||
import { Resolved, Router } from '@/nirax.js';
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef } from 'vue';
|
||||
import { IRouter, Resolved } from '@/nirax.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
router?: Router;
|
||||
router?: IRouter;
|
||||
}>();
|
||||
|
||||
const router = props.router ?? inject('router');
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { inject } from 'vue';
|
||||
import { IRouter, Router } from '@/nirax.js';
|
||||
import { mainRouter } from '@/global/router/main.js';
|
||||
|
||||
/**
|
||||
* メインの{@link Router}を取得する。
|
||||
* あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
|
||||
*/
|
||||
export function useRouter(): IRouter {
|
||||
return inject<Router | null>('router', null) ?? mainRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意の{@link Router}を取得するためのファクトリを取得する。
|
||||
* あらかじめ{@link setupRouter}を実行しておく必要がある。
|
||||
*/
|
||||
export function useRouterFactory(): (path: string) => IRouter {
|
||||
const factory = inject<(path: string) => IRouter>('routerFactory');
|
||||
if (!factory) {
|
||||
console.error('routerFactory is not defined.');
|
||||
throw new Error('routerFactory is not defined.');
|
||||
}
|
||||
|
||||
return factory;
|
||||
}
|