1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-06-07 22:04:10 +02:00

Merge branch 'develop' into copilot/add-user-mute-settings

This commit is contained in:
かっこかり
2026-04-02 11:30:10 +09:00
committed by GitHub
702 changed files with 16645 additions and 10925 deletions

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -9,7 +9,7 @@ window.onload = async () => {
const account = JSON.parse(localStorage.getItem('account'));
const i = account.token;
const api = (endpoint, data = {}) => {
const _api = (endpoint, data = {}) => {
const promise = new Promise((resolve, reject) => {
// Append a credential
if (i) data.i = i;

121
packages/backend/build.js Normal file
View File

@@ -0,0 +1,121 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { build } from 'esbuild';
import { swcPlugin } from 'esbuild-plugin-swc';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
const resolveTsPathsPlugin = {
name: 'resolve-ts-paths',
setup(build) {
build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => {
if (args.importer) {
const absPath = join(args.resolveDir, args.path);
const tsPath = absPath.slice(0, -3) + '.ts';
if (fs.existsSync(tsPath)) return { path: tsPath };
const tsxPath = absPath.slice(0, -3) + '.tsx';
if (fs.existsSync(tsxPath)) return { path: tsxPath };
}
});
},
};
const externalIpaddrPlugin = {
name: 'external-ipaddr',
setup(build) {
build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => {
return { path: args.path, external: true };
});
},
};
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints: ['./src/boot/entry.ts'],
minify: true,
keepNames: true,
bundle: true,
outdir: './built/boot',
target: 'node22',
platform: 'node',
format: 'esm',
sourcemap: 'linked',
packages: 'external',
banner: {
js: 'import { createRequire as topLevelCreateRequire } from "module";' +
'import ___url___ from "url";' +
'const require = topLevelCreateRequire(import.meta.url);' +
'const __filename = ___url___.fileURLToPath(import.meta.url);' +
'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));',
},
plugins: [
externalIpaddrPlugin,
resolveTsPathsPlugin,
swcPlugin({
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
dynamicImport: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
},
experimental: {
keepImportAssertions: true,
},
baseUrl: join(_dirname, 'src'),
paths: {
'@/*': ['*'],
},
target: 'esnext',
keepClassNames: true,
},
}),
externalIpaddrPlugin,
],
// external: [
// 'slacc-*',
// 'class-transformer',
// 'class-validator',
// '@sentry/*',
// '@nestjs/websockets/socket-module',
// '@nestjs/microservices/microservices-module',
// '@nestjs/microservices',
// '@napi-rs/canvas-win32-x64-msvc',
// 'mock-aws-s3',
// 'aws-sdk',
// 'nock',
// 'sharp',
// 'jsdom',
// 're2',
// '@napi-rs/canvas',
// ],
};
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
if (!args.includes('--no-clean')) {
fs.rmSync('./built', { recursive: true, force: true });
}
await buildSrc();
async function buildSrc() {
console.log(`[${_package.name}] start building...`);
await build(options)
.then(() => {
console.log(`[${_package.name}] build succeeded.`);
})
.catch((err) => {
process.stderr.write(err.stderr || err.message || err);
process.exit(1);
});
console.log(`[${_package.name}] finish building.`);
}

View File

@@ -25,7 +25,6 @@ export default [
},
},
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'import/order': ['warn', {
groups: [
'builtin',

View File

@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class BirthdayIndex1767169026317 {
name = 'BirthdayIndex1767169026317'
async up(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_de22cd2b445eee31ae51cdbe99"`);
await queryRunner.query(`CREATE OR REPLACE FUNCTION get_birthday_date(birthday TEXT) RETURNS SMALLINT AS $$ BEGIN RETURN CAST((SUBSTR(birthday, 6, 2) || SUBSTR(birthday, 9, 2)) AS SMALLINT); END; $$ LANGUAGE plpgsql IMMUTABLE;`);
await queryRunner.query(`CREATE INDEX "IDX_USERPROFILE_BIRTHDAY_DATE" ON "user_profile" (get_birthday_date("birthday"))`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_de22cd2b445eee31ae51cdbe99" ON "user_profile" (substr("birthday", 6, 5))`);
await queryRunner.query(`DROP INDEX "public"."IDX_USERPROFILE_BIRTHDAY_DATE"`);
await queryRunner.query(`DROP FUNCTION IF EXISTS get_birthday_date(birthday TEXT)`);
}
}

View File

@@ -1,6 +1,6 @@
import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js';
import { entities } from './built/postgres.js';
import { loadConfig } from './src-js/config.js';
import { entities } from './src-js/postgres.js';
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';

View File

@@ -12,17 +12,17 @@
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
"cli": "pnpm compile-config && node ./built/boot/cli.js",
"cli": "pnpm compile-config && node ./src-js/boot/cli.js",
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
"compile-config": "node ./scripts/compile_config.js",
"build": "swc src -d built -D --strip-leading-paths",
"build": "swc src -d src-js -D --strip-leading-paths && node ./build.js",
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start",
"dev": "pnpm compile-config && node ./scripts/dev.mjs",
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
"typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit",
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
@@ -41,20 +41,20 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.15.3",
"@swc/core-darwin-x64": "1.15.3",
"@swc/core-darwin-arm64": "1.15.21",
"@swc/core-darwin-x64": "1.15.21",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.15.3",
"@swc/core-linux-arm64-gnu": "1.15.3",
"@swc/core-linux-arm64-musl": "1.15.3",
"@swc/core-linux-x64-gnu": "1.15.3",
"@swc/core-linux-x64-musl": "1.15.3",
"@swc/core-win32-arm64-msvc": "1.15.3",
"@swc/core-win32-ia32-msvc": "1.15.3",
"@swc/core-win32-x64-msvc": "1.15.3",
"@swc/core-linux-arm-gnueabihf": "1.15.21",
"@swc/core-linux-arm64-gnu": "1.15.21",
"@swc/core-linux-arm64-musl": "1.15.21",
"@swc/core-linux-x64-gnu": "1.15.21",
"@swc/core-linux-x64-musl": "1.15.21",
"@swc/core-win32-arm64-msvc": "1.15.21",
"@swc/core-win32-ia32-msvc": "1.15.21",
"@swc/core-win32-x64-msvc": "1.15.21",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
"bufferutil": "4.1.0",
"slacc-android-arm-eabi": "0.0.10",
"slacc-android-arm64": "0.0.10",
"slacc-darwin-arm64": "0.0.10",
@@ -68,43 +68,42 @@
"slacc-linux-x64-musl": "0.0.10",
"slacc-win32-arm64-msvc": "0.0.10",
"slacc-win32-x64-msvc": "0.0.10",
"utf-8-validate": "6.0.5"
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.947.0",
"@aws-sdk/lib-storage": "3.947.0",
"@aws-sdk/client-s3": "3.1016.0",
"@aws-sdk/lib-storage": "3.1016.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "11.4.1",
"@fastify/multipart": "9.3.0",
"@fastify/static": "8.3.0",
"@kitajs/html": "4.2.11",
"@fastify/cors": "11.2.0",
"@fastify/express": "4.0.4",
"@fastify/http-proxy": "11.4.2",
"@fastify/multipart": "9.4.0",
"@fastify/static": "9.0.0",
"@kitajs/html": "4.2.13",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
"@napi-rs/canvas": "0.1.84",
"@nestjs/common": "11.1.9",
"@nestjs/core": "11.1.9",
"@nestjs/testing": "11.1.9",
"@napi-rs/canvas": "0.1.97",
"@nestjs/common": "11.1.17",
"@nestjs/core": "11.1.17",
"@nestjs/testing": "11.1.17",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.29.0",
"@sentry/profiling-node": "10.29.0",
"@simplewebauthn/server": "13.2.2",
"@sinonjs/fake-timers": "15.0.0",
"@smithy/node-http-handler": "4.4.5",
"@swc/cli": "0.7.9",
"@swc/core": "1.15.3",
"@sentry/node": "10.45.0",
"@sentry/profiling-node": "10.45.0",
"@simplewebauthn/server": "13.3.0",
"@sinonjs/fake-timers": "15.1.1",
"@smithy/node-http-handler": "4.5.0",
"@swc/cli": "0.8.0",
"@swc/core": "1.15.21",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
"ajv": "8.17.1",
"ajv": "8.18.0",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.1",
"bullmq": "5.65.1",
"body-parser": "2.2.2",
"bullmq": "5.71.0",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@@ -113,76 +112,74 @@
"content-disposition": "1.0.1",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
"fastify": "5.6.2",
"fastify": "5.8.4",
"fastify-raw-body": "5.0.0",
"feed": "5.1.0",
"file-type": "21.1.1",
"feed": "5.2.0",
"file-type": "21.3.4",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5",
"got": "14.6.5",
"got": "14.6.6",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
"ioredis": "5.8.2",
"ioredis": "5.10.1",
"ip-cidr": "4.0.2",
"ipaddr.js": "2.3.0",
"is-svg": "6.1.0",
"json5": "2.2.3",
"jsonld": "9.0.0",
"juice": "11.0.3",
"meilisearch": "0.54.0",
"juice": "11.1.1",
"meilisearch": "0.56.0",
"mfm-js": "0.25.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.6",
"nanoid": "5.1.7",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "7.0.1",
"nodemailer": "7.0.11",
"nsfwjs": "4.2.0",
"node-html-parser": "7.1.0",
"nodemailer": "8.0.3",
"nsfwjs": "4.3.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.4.1",
"pg": "8.16.3",
"pkce-challenge": "5.0.1",
"otpauth": "9.5.0",
"pg": "8.20.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.22.3",
"redis-info": "3.1.0",
"re2": "1.23.3",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.2",
"sanitize-html": "2.17.0",
"sanitize-html": "2.17.2",
"secure-json-parse": "4.1.0",
"semver": "7.7.3",
"semver": "7.7.4",
"sharp": "0.33.5",
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.27.12",
"systeminformation": "5.31.5",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
"typeorm": "0.3.28",
"typescript": "5.9.3",
"ulid": "3.0.2",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.18.3",
"ws": "8.20.0",
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.3",
"@nestjs/platform-express": "11.1.9",
"@sentry/vue": "10.29.0",
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.17",
"@sentry/vue": "10.45.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@@ -196,18 +193,18 @@
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/node": "24.12.0",
"@types/nodemailer": "7.0.11",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.15.6",
"@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0",
"@types/sanitize-html": "2.16.1",
"@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.7",
"@types/simple-oauth2": "5.0.8",
"@types/sinonjs__fake-timers": "15.0.1",
"@types/supertest": "6.0.3",
"@types/tinycolor2": "1.4.6",
@@ -215,21 +212,22 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.11",
"cbor": "10.0.12",
"cross-env": "10.1.0",
"esbuild-plugin-swc": "1.0.1",
"eslint-plugin-import": "2.32.0",
"execa": "9.6.1",
"fkill": "10.0.1",
"fkill": "10.0.3",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"js-yaml": "4.1.1",
"nodemon": "3.1.11",
"pid-port": "2.0.0",
"nodemon": "3.1.14",
"pid-port": "2.1.0",
"simple-oauth2": "5.1.0",
"supertest": "7.1.4",
"vite": "7.2.7"
"supertest": "7.2.2",
"vite": "8.0.2"
}
}

View File

@@ -4,8 +4,8 @@
*/
import Redis from 'ioredis';
import { loadConfig } from '../built/config.js';
import { createPostgresDataSource } from '../built/postgres.js';
import { loadConfig } from '../src-js/config.js';
import { createPostgresDataSource } from '../src-js/postgres.js';
const config = loadConfig();
@@ -16,26 +16,22 @@ async function connectToPostgres() {
}
async function connectToRedis(redisOptions) {
return await new Promise(async (resolve, reject) => {
const redis = new Redis({
let redis;
try {
redis = new Redis({
...redisOptions,
lazyConnect: true,
reconnectOnError: false,
showFriendlyErrorStack: true,
});
redis.on('error', e => reject(e));
try {
await redis.connect();
resolve();
} catch (e) {
reject(e);
} finally {
redis.disconnect(false);
}
});
await Promise.race([
new Promise((_, reject) => redis.on('error', e => reject(e))),
redis.connect(),
]);
} finally {
redis.disconnect(false);
}
}
// If not all of these are defined, the default one gets reused.
@@ -50,7 +46,7 @@ const promises = Array
]))
.map(connectToRedis)
.concat([
connectToPostgres()
connectToPostgres(),
]);
await Promise.all(promises);

View File

@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { writeFileSync, existsSync } from 'node:fs';
import { execa } from 'execa';
import { writeFileSync, existsSync } from "node:fs";
async function main() {
if (!process.argv.includes('--no-build')) {
@@ -19,10 +19,10 @@ async function main() {
}
/** @type {import('../src/config.js')} */
const { loadConfig } = await import('../built/config.js');
const { loadConfig } = await import('../src-js/config.js');
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js');
const config = loadConfig();
const spec = genOpenapiSpec(config, true);

View File

@@ -14,24 +14,56 @@ import { fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import * as http from 'node:http';
import * as fs from 'node:fs/promises';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const SAMPLE_COUNT = 3; // Number of samples to measure
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
async function measureMemory() {
const startTime = Date.now();
const keys = {
VmPeak: 0,
VmSize: 0,
VmHWM: 0,
VmRSS: 0,
VmData: 0,
VmStk: 0,
VmExe: 0,
VmLib: 0,
VmPTE: 0,
VmSwap: 0,
};
async function getMemoryUsage(pid) {
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
const result = {};
for (const key of Object.keys(keys)) {
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
if (match) {
result[key] = parseInt(match[1], 10);
} else {
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
}
}
return result;
}
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
cwd: join(__dirname, '..'),
env: {
...process.env,
NODE_ENV: 'test',
NODE_ENV: 'production',
MK_DISABLE_CLUSTERING: '1',
},
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
execArgv: [...process.execArgv, '--expose-gc'],
});
let serverReady = false;
@@ -57,6 +89,40 @@ async function measureMemory() {
process.stderr.write(`[server error] ${err}\n`);
});
async function triggerGc() {
const ok = new Promise((resolve) => {
serverProcess.once('message', (message) => {
if (message === 'gc ok') resolve();
});
});
serverProcess.send('gc');
await ok;
await setTimeout(1000);
}
function createRequest() {
return new Promise((resolve, reject) => {
const req = http.request({
host: 'localhost',
port: 61812,
path: '/api/meta',
method: 'POST',
}, (res) => {
res.on('data', () => { });
res.on('end', () => {
resolve();
});
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
// Wait for server to be ready or timeout
const startupStartTime = Date.now();
while (!serverReady) {
@@ -73,46 +139,23 @@ async function measureMemory() {
// Wait for memory to settle
await setTimeout(MEMORY_SETTLE_TIME);
// Get memory usage from the server process via /proc
const pid = serverProcess.pid;
let memoryInfo;
try {
const fs = await import('node:fs/promises');
const beforeGc = await getMemoryUsage(pid);
// Read /proc/[pid]/status for detailed memory info
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
await triggerGc();
memoryInfo = {
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
};
} catch (err) {
// Fallback: use ps command
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
const afterGc = await getMemoryUsage(pid);
const { execSync } = await import('node:child_process');
try {
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
const rssKb = parseInt(ps.trim(), 10);
memoryInfo = {
rss: rssKb * 1024,
heapUsed: null,
vmSize: null,
};
} catch {
memoryInfo = {
rss: null,
heapUsed: null,
vmSize: null,
error: 'Could not measure memory',
};
}
}
// create some http requests to simulate load
const REQUEST_COUNT = 10;
await Promise.all(
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
);
await triggerGc();
const afterRequest = await getMemoryUsage(pid);
// Stop the server
serverProcess.kill('SIGTERM');
@@ -135,15 +178,51 @@ async function measureMemory() {
const result = {
timestamp: new Date().toISOString(),
startupTimeMs: startupTime,
memory: memoryInfo,
beforeGc,
afterGc,
afterRequest,
};
return result;
}
async function main() {
// 直列の方が時間的に分散されて正確そうだから直列でやる
const results = [];
for (let i = 0; i < SAMPLE_COUNT; i++) {
const res = await measureMemory();
results.push(res);
}
// Calculate averages
const beforeGc = structuredClone(keys);
const afterGc = structuredClone(keys);
const afterRequest = structuredClone(keys);
for (const res of results) {
for (const key of Object.keys(keys)) {
beforeGc[key] += res.beforeGc[key];
afterGc[key] += res.afterGc[key];
afterRequest[key] += res.afterRequest[key];
}
}
for (const key of Object.keys(keys)) {
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
}
const result = {
timestamp: new Date().toISOString(),
beforeGc,
afterGc,
afterRequest,
};
// Output as JSON to stdout
console.log(JSON.stringify(result, null, 2));
}
measureMemory().catch((err) => {
main().catch((err) => {
console.error(JSON.stringify({
error: err.message,
timestamp: new Date().toISOString(),

View File

@@ -21,7 +21,7 @@ import { execa } from 'execa';
});
}, 3000);
execa('tsc', ['-w', '-p', 'tsconfig.json'], {
execa('tsgo', ['-w', '-p', 'tsconfig.json'], {
stdout: process.stdout,
stderr: process.stderr,
});

View File

@@ -86,6 +86,18 @@ if (!envOption.disableClustering) {
ev.mount();
}
process.on('message', msg => {
if (msg === 'gc') {
if (global.gc != null) {
logger.info('Manual GC triggered');
global.gc();
if (process.send != null) process.send('gc ok');
} else {
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
}
}
});
readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため

View File

@@ -4,8 +4,6 @@
*/
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
@@ -17,20 +15,15 @@ import { showMachineInfo } from '@/misc/show-machine-info.js';
import { envOption } from '@/env.js';
import { jobQueue, server } from './common.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta');
const themeColor = chalk.hex('#86b300');
function greet() {
function greet(props: { version: string }) {
if (!envOption.quiet) {
//#region Misskey logo
const v = `v${meta.version}`;
const v = `v${props.version}`;
console.log(themeColor(' _____ _ _ '));
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
@@ -46,7 +39,7 @@ function greet() {
}
bootLogger.info('Welcome to Misskey!');
bootLogger.info(`Misskey v${meta.version}`, null, true);
bootLogger.info(`Misskey v${props.version}`, null, true);
}
/**
@@ -57,15 +50,15 @@ export async function masterMain() {
// initialize app
try {
greet();
config = loadConfigBoot();
greet({ version: config.version });
showEnvironment();
await showMachineInfo(bootLogger);
showNodejsVersion();
config = loadConfigBoot();
//await connectDb();
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true);
bootLogger.error('Fatal error occurred during initialization: ' + e, null, true);
process.exit(1);
}

View File

@@ -10,7 +10,6 @@ import { type FastifyServerOptions } from 'fastify';
import type * as Sentry from '@sentry/node';
import type * as SentryVue from '@sentry/vue';
import type { RedisOptions } from 'ioredis';
import type { ManifestChunk } from 'vite';
type RedisOptionsSource = Partial<RedisOptions> & {
host: string;
@@ -30,6 +29,7 @@ type Source = {
socket?: string;
trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string;
enableIpRateLimit?: boolean;
disableHsts?: boolean;
db: {
host: string;
@@ -120,8 +120,9 @@ export type Config = {
url: string;
port: number;
socket: string | undefined;
trustProxy: FastifyServerOptions['trustProxy'];
trustProxy: NonNullable<FastifyServerOptions['trustProxy']>;
chmodSocket: string | undefined;
enableIpRateLimit: boolean;
disableHsts: boolean | undefined;
db: {
host: string;
@@ -187,9 +188,7 @@ export type Config = {
authUrl: string;
driveUrl: string;
userAgent: string;
frontendEntry: ManifestChunk;
frontendManifestExists: boolean;
frontendEmbedEntry: ManifestChunk;
frontendEmbedManifestExists: boolean;
mediaProxy: string;
externalMediaProxyEnabled: boolean;
@@ -217,25 +216,37 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
/** Path of repository root directory */
let rootDir = _dirname;
// 見つかるまで上に遡る
while (!fs.existsSync(resolve(rootDir, 'packages'))) {
const parentDir = dirname(rootDir);
if (parentDir === rootDir) {
throw new Error('Cannot find root directory');
}
rootDir = parentDir;
}
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
/** Path of configuration directory */
const configDir = resolve(rootDir, '.config');
/** Path of built directory */
const projectBuiltDir = resolve(rootDir, 'built');
const compiledConfigFilePathForTest = resolve(projectBuiltDir, '._config_.json');
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest)
? compiledConfigFilePathForTest
: resolve(projectBuiltDir, '.config.json');
export function loadConfig(): Config {
if (!fs.existsSync(compiledConfigFilePath)) {
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
}
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
const meta = JSON.parse(fs.readFileSync(resolve(projectBuiltDir, 'meta.json'), 'utf-8'));
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
const frontendManifest = frontendManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
: { 'src/_boot_.ts': { file: null } };
const frontendEmbedManifest = frontendEmbedManifestExists ?
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
: { 'src/boot.ts': { file: null } };
const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
@@ -263,9 +274,17 @@ export function loadConfig(): Config {
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket,
trustProxy: config.trustProxy,
trustProxy: config.trustProxy ?? [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.1/32',
'::1/128',
'fc00::/7',
],
chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts,
enableIpRateLimit: config.enableIpRateLimit ?? true,
host,
hostname,
scheme,
@@ -309,9 +328,7 @@ export function loadConfig(): Config {
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
: null,
userAgent: `Misskey/${version} (${config.url})`,
frontendEntry: frontendManifest['src/_boot_.ts'],
frontendManifestExists: frontendManifestExists,
frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
frontendEmbedManifestExists: frontendEmbedManifestExists,
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
@@ -324,7 +341,7 @@ export function loadConfig(): Config {
function tryCreateUrl(url: string) {
try {
return new URL(url);
} catch (e) {
} catch (_) {
throw new Error(`url="${url}" is not a valid URL.`);
}
}

View File

@@ -75,7 +75,7 @@ export class AccountMoveService {
*/
@bindThis
public async moveFromLocal(src: MiLocalUser, dst: MiLocalUser | MiRemoteUser): Promise<unknown> {
const srcUri = this.userEntityService.getUserUri(src);
const _srcUri = this.userEntityService.getUserUri(src);
const dstUri = this.userEntityService.getUserUri(dst);
// add movedToUri to indicate that the user has moved

View File

@@ -7,11 +7,10 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Injectable } from '@nestjs/common';
import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import fetch from 'node-fetch';
import { bindThis } from '@/decorators.js';
import type { NSFWJS, PredictionType } from 'nsfwjs';
import type { NSFWJS, PredictionType } from 'nsfwjs/core';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@@ -44,7 +43,7 @@ export class AiService {
tf.env().global.fetch = fetch;
if (this.model == null) {
const nsfw = await import('nsfwjs');
const nsfw = await import('nsfwjs/core');
await this.modelLoadMutex.runExclusive(async () => {
if (this.model == null) {
this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 });
@@ -84,6 +83,7 @@ export class AiService {
@bindThis
private async getCpuFlags(): Promise<string[]> {
const si = await import('systeminformation');
const str = await si.cpuFlags();
return str.split(/\s+/);
}

View File

@@ -205,7 +205,7 @@ export class AnnouncementService {
announcementId: announcementId,
userId: user.id,
});
} catch (e) {
} catch (_) {
return;
}

View File

@@ -39,7 +39,7 @@ export class AvatarDecorationService implements OnApplicationShutdown {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
const { type, body: _ } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'avatarDecorationCreated':
case 'avatarDecorationUpdated':

View File

@@ -141,7 +141,7 @@ import { ApLoggerService } from './activitypub/ApLoggerService.js';
import { ApMfmService } from './activitypub/ApMfmService.js';
import { ApRendererService } from './activitypub/ApRendererService.js';
import { ApRequestService } from './activitypub/ApRequestService.js';
import { ApResolverService } from './activitypub/ApResolverService.js';
import { ApResolverService, Resolver } from './activitypub/ApResolverService.js';
import { JsonLdService } from './activitypub/JsonLdService.js';
import { RemoteLoggerService } from './RemoteLoggerService.js';
import { RemoteUserResolveService } from './RemoteUserResolveService.js';
@@ -447,6 +447,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
Resolver,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,
@@ -745,6 +746,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
ApRendererService,
ApRequestService,
ApResolverService,
Resolver,
JsonLdService,
RemoteLoggerService,
RemoteUserResolveService,

View File

@@ -366,7 +366,7 @@ export class EmailService {
valid: true,
reason: null,
};
} catch (error) {
} catch (_) {
return {
valid: false,
reason: 'network',

View File

@@ -484,25 +484,13 @@ export class FileInfoService {
* Calculate blurhash string of image
*/
@bindThis
private getBlurhash(path: string, type: string): Promise<string> {
return new Promise(async (resolve, reject) => {
(await sharpBmp(path, type))
.raw()
.ensureAlpha()
.resize(64, 64, { fit: 'inside' })
.toBuffer((err, buffer, info) => {
if (err) return reject(err);
let hash;
try {
hash = blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
} catch (e) {
return reject(e);
}
resolve(hash);
});
});
private async getBlurhash(path: string, type: string): Promise<string> {
const sharp = await sharpBmp(path, type);
const { data: buffer, info } = await sharp
.raw()
.ensureAlpha()
.resize(64, 64, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
return blurhash.encode(new Uint8ClampedArray(buffer), info.width, info.height, 5, 5);
}
}

View File

@@ -38,11 +38,7 @@ export interface BroadcastTypes {
emojis: Packed<'EmojiDetailed'>[];
};
emojiDeleted: {
emojis: {
id?: string;
name: string;
[other: string]: any;
}[];
emojis: Packed<'EmojiDetailed'>[];
};
announcementCreated: {
announcement: Packed<'Announcement'>;
@@ -133,6 +129,9 @@ export interface NoteEventTypes {
type NoteStreamEventTypes = {
[key in keyof NoteEventTypes]: {
id: MiNote['id'];
userId: MiNote['userId'];
visibility: MiNote['visibility'];
visibleUserIds: MiNote['visibleUserIds'];
body: NoteEventTypes[key];
};
};
@@ -382,9 +381,12 @@ export class GlobalEventService {
}
@bindThis
public publishNoteStream<K extends keyof NoteEventTypes>(noteId: MiNote['id'], type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${noteId}`, type, {
id: noteId,
public publishNoteStream<K extends keyof NoteEventTypes>(note: MiNote, type: K, value?: NoteEventTypes[K]): void {
this.publish(`noteStream:${note.id}`, type, {
id: note.id,
userId: note.userId,
visibility: note.visibility,
visibleUserIds: note.visibleUserIds,
body: value,
});
}

View File

@@ -308,7 +308,7 @@ export class MfmService {
try {
const date = new Date(parseInt(text, 10) * 1000);
return `<time datetime="${escapeHtml(date.toISOString())}">${escapeHtml(date.toISOString())}</time>`;
} catch (err) {
} catch (_) {
return fnDefault(node);
}
}
@@ -376,7 +376,7 @@ export class MfmService {
try {
const url = new URL(node.props.url);
return `<a href="${escapeHtml(url.href)}">${toHtml(node.children)}</a>`;
} catch (err) {
} catch (_) {
return `[${toHtml(node.children)}](${escapeHtml(node.props.url)})`;
}
},
@@ -390,7 +390,7 @@ export class MfmService {
try {
const url = new URL(href);
return `<a href="${escapeHtml(url.href)}" class="u-url mention">${escapeHtml(acct)}</a>`;
} catch (err) {
} catch (_) {
return escapeHtml(acct);
}
},
@@ -419,7 +419,7 @@ export class MfmService {
try {
const url = new URL(node.props.url);
return `<a href="${escapeHtml(url.href)}">${escapeHtml(node.props.url)}</a>`;
} catch (err) {
} catch (_) {
return escapeHtml(node.props.url);
}
},

View File

@@ -68,7 +68,7 @@ export class NoteDeleteService {
}
if (!quiet) {
this.globalEventService.publishNoteStream(note.id, 'deleted', {
this.globalEventService.publishNoteStream(note, 'deleted', {
deletedAt: deletedAt,
});

View File

@@ -187,9 +187,9 @@ export class NoteDraftService {
}
//#region visibleUsers
let visibleUsers: MiUser[] = [];
let _visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
visibleUsers = await this.usersRepository.findBy({
_visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
}

View File

@@ -83,7 +83,7 @@ export class PollService {
const index = choice + 1; // In SQL, array index is 1 based
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
this.globalEventService.publishNoteStream(note, 'pollVoted', {
choice: choice,
userId: user.id,
});

View File

@@ -259,7 +259,7 @@ export class QueryService {
@bindThis
public generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: MiUser['id'] } | null): void {
// This code must always be synchronized with the checks in Notes.isVisibleForMe.
// This code must always be synchronized with the checks in NoteEntityService.isVisibleForMe and Stream abstract class Channel.isNoteVisibleForMe.
if (me == null) {
q.andWhere(new Brackets(qb => {
qb

View File

@@ -6,7 +6,6 @@
import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import { MetricsTime, type JobType } from 'bullmq';
import { parse as parseRedisInfo } from 'redis-info';
import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
@@ -86,6 +85,19 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
pattern: '0 4 * * *',
}];
function parseRedisInfo(infoText: string): Record<string, string> {
const fields = infoText
.split('\n')
.filter(line => line.length > 0 && !line.startsWith('#'))
.map(line => line.trim().split(':'));
const result: Record<string, string> = {};
for (const [key, value] of fields) {
result[key] = value;
}
return result;
}
@Injectable()
export class QueueService {
constructor(
@@ -890,7 +902,7 @@ export class QueueService {
},
db: {
version: db.redis_version,
mode: db.redis_mode,
mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
runId: db.run_id,
processId: db.process_id,
port: parseInt(db.tcp_port),

View File

@@ -244,7 +244,7 @@ export class ReactionService {
},
});
this.globalEventService.publishNoteStream(note.id, 'reacted', {
this.globalEventService.publishNoteStream(note, 'reacted', {
reaction: decodedReaction.reaction,
emoji: customEmoji != null ? {
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
@@ -318,7 +318,7 @@ export class ReactionService {
.execute();
}
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
this.globalEventService.publishNoteStream(note, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
userId: user.id,
});

View File

@@ -314,7 +314,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
default:
return false;
}
} catch (err) {
} catch (_) {
// TODO: log error
return false;
}

View File

@@ -190,8 +190,7 @@ export class SearchService {
return this.searchNoteByMeiliSearch(q, me, opts, pagination);
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const typeCheck: never = this.provider;
const _: never = this.provider;
return [];
}
}

View File

@@ -49,8 +49,8 @@ export class UserSuspendService {
});
(async () => {
await this.postSuspend(user).catch(e => {});
await this.unFollowAll(user).catch(e => {});
await this.postSuspend(user).catch(_ => {});
await this.unFollowAll(user).catch(_ => {});
})();
}
@@ -67,7 +67,7 @@ export class UserSuspendService {
});
(async () => {
await this.postUnsuspend(user).catch(e => {});
await this.postUnsuspend(user).catch(_ => {});
})();
}

View File

@@ -98,7 +98,7 @@ export class UtilityService {
try {
// TODO: RE2インスタンスをキャッシュ
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
} catch (_) {
// This should never happen due to input sanitisation.
return false;
}

View File

@@ -95,7 +95,7 @@ export class ApInboxService {
if (isCollectionOrOrderedCollection(activity)) {
const results = [] as [string, string | void][];
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
if (items.length >= resolver.getRecursionLimit()) {
@@ -221,7 +221,7 @@ export class ApInboxService {
this.logger.info(`Accept: ${uri}`);
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(err => {
this.logger.error(`Resolution failed: ${err}`);
@@ -284,7 +284,7 @@ export class ApInboxService {
this.logger.info(`Announce: ${uri}`);
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
if (!activity.object) return 'skip: activity has no object property';
const targetUri = getApId(activity.object);
@@ -406,7 +406,7 @@ export class ApInboxService {
}
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -575,7 +575,7 @@ export class ApInboxService {
this.logger.info(`Reject: ${uri}`);
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -642,7 +642,7 @@ export class ApInboxService {
this.logger.info(`Undo: ${uri}`);
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);
@@ -774,7 +774,7 @@ export class ApInboxService {
this.logger.debug('Update');
// eslint-disable-next-line no-param-reassign
resolver ??= this.apResolverService.createResolver();
resolver ??= await this.apResolverService.createResolver();
const object = await resolver.resolve(activity.object).catch(e => {
this.logger.error(`Resolution failed: ${e}`);

View File

@@ -515,7 +515,7 @@ export class ApRendererService {
const restPart = maybeUrl.slice(match[0].length);
return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
} catch (e) {
} catch (_) {
return maybeUrl;
}
};

View File

@@ -81,7 +81,7 @@ export class ApRequestCreator {
}, args.additionalHeaders),
};
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host']);
return {
request,
@@ -226,7 +226,7 @@ export class ApRequestService {
return await this.signedGet(href, user, allowSoftfail, false);
}
}
} catch (e) {
} catch (_) {
// something went wrong parsing the HTML, ignore the whole thing
}
}

View File

@@ -3,10 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { IsNull, Not } from 'typeorm';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
import type {
FollowRequestsRepository,
MiMeta,
NoteReactionsRepository,
NotesRepository,
PollsRepository,
UsersRepository
} from '@/models/_.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { DI } from '@/di-symbols.js';
@@ -16,26 +23,43 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { ICollection, IObject, IOrderedCollection } from './type.js';
import { isCollectionOrOrderedCollection } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js';
import { FetchAllowSoftFailMask } from './misc/check-against-url.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js';
import { ModuleRef } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class Resolver {
private history: Set<string>;
private user?: MiLocalUser;
private logger: Logger;
private recursionLimit = 256;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService,
private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService,
@@ -43,7 +67,6 @@ export class Resolver {
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
private recursionLimit = 256,
) {
this.history = new Set();
this.logger = this.loggerService.getLogger('ap-resolve');
@@ -180,54 +203,12 @@ export class Resolver {
@Injectable()
export class ApResolverService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.pollsRepository)
private pollsRepository: PollsRepository,
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,
private utilityService: UtilityService,
private systemAccountService: SystemAccountService,
private apRequestService: ApRequestService,
private httpRequestService: HttpRequestService,
private apRendererService: ApRendererService,
private apDbResolverService: ApDbResolverService,
private loggerService: LoggerService,
private moduleRef: ModuleRef,
) {
}
@bindThis
public createResolver(): Resolver {
return new Resolver(
this.config,
this.meta,
this.usersRepository,
this.notesRepository,
this.pollsRepository,
this.noteReactionsRepository,
this.followRequestsRepository,
this.utilityService,
this.systemAccountService,
this.apRequestService,
this.httpRequestService,
this.apRendererService,
this.apDbResolverService,
this.loggerService,
);
public async createResolver(): Promise<Resolver> {
return await this.moduleRef.create(Resolver);
}
}

View File

@@ -46,7 +46,7 @@ export class ApImageService {
throw new Error('actor has been suspended');
}
const image = await this.apResolverService.createResolver().resolve(value);
const image = await (await this.apResolverService.createResolver()).resolve(value);
if (!isDocument(image)) return null;

View File

@@ -128,7 +128,7 @@ export class ApNoteService {
@bindThis
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(value);

View File

@@ -310,7 +310,7 @@ export class ApPersonService implements OnModuleInit {
}
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
@@ -500,7 +500,7 @@ export class ApPersonService implements OnModuleInit {
//#endregion
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
const object = hint ?? await resolver.resolve(uri);
@@ -678,7 +678,7 @@ export class ApPersonService implements OnModuleInit {
// リモートサーバーからフェッチしてきて登録
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
return await this.createPerson(uri, resolver);
}
@@ -707,7 +707,7 @@ export class ApPersonService implements OnModuleInit {
this.logger.info(`Updating the featured: ${user.uri}`);
const _resolver = resolver ?? this.apResolverService.createResolver();
const _resolver = resolver ?? await this.apResolverService.createResolver();
// Resolve to (Ordered)Collection Object
const collection = await _resolver.resolveCollection(user.featured);

View File

@@ -45,7 +45,7 @@ export class ApQuestionService {
@bindThis
public async extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(source);
if (!isQuestion(question)) throw new Error('invalid type');
@@ -91,7 +91,7 @@ export class ApQuestionService {
// resolve new Question object
// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = this.apResolverService.createResolver();
if (resolver == null) resolver = await this.apResolverService.createResolver();
const question = await resolver.resolve(value);
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);

View File

@@ -138,7 +138,7 @@ export class ChatEntityService {
const reactions: { reaction: string; }[] = [];
for (const record of message.reactions) {
const [userId, reaction] = record.split('/');
const [, reaction] = record.split('/');
reactions.push({
reaction,
});

View File

@@ -17,6 +17,7 @@ import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { IdService } from '@/core/IdService.js';
import { uniqueByKey } from '@/misc/unique-by-key.js';
import { UtilityService } from '../UtilityService.js';
import { VideoProcessingService } from '../VideoProcessingService.js';
import { UserEntityService } from './UserEntityService.js';
@@ -226,6 +227,7 @@ export class DriveFileEntityService {
options?: PackOptions,
hint?: {
packedUser?: Packed<'UserLite'>
packedFolder?: Packed<'DriveFolder'>
},
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
@@ -250,9 +252,9 @@ export class DriveFileEntityService {
thumbnailUrl: this.getThumbnailUrl(file),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
folder: opts.detail && file.folderId ? (hint?.packedFolder ?? this.driveFolderEntityService.pack(file.folderId, {
detail: true,
}) : null,
})) : null,
userId: file.userId,
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
@@ -263,10 +265,41 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
// -- ユーザ情報の事前取得 --
let userMap: Map<string, Packed<'UserLite'>> | null = null;
if (options?.withUser) {
const users = files
.map(({ user, userId }) => user ?? userId)
.filter(x => x != null);
const uniqueUsers = uniqueByKey(users, (user) => typeof user === 'string' ? user : user.id);
const packedUsers = await this.userEntityService.packMany(uniqueUsers);
userMap = new Map(packedUsers.map(user => [user.id, user]));
}
// -- フォルダ情報の事前取得 --
let folderMap: Map<string, Packed<'DriveFolder'>> | null = null;
if (options?.detail) {
const folders = files
.map(({ folder, folderId }) => folder ?? folderId)
.filter(x => x != null);
const uniqueFolders = uniqueByKey(folders, (folder) => typeof folder === 'string' ? folder : folder.id);
const packedFolders = await this.driveFolderEntityService.packMany(uniqueFolders, { detail: true });
folderMap = new Map(packedFolders.map(folder => [folder.id, folder]));
}
const items = await Promise.all(files.map(f => this.packNullable(
f,
options,
{
packedUser: f.userId ? userMap?.get(f.userId) : undefined,
packedFolder: f.folderId ? folderMap?.get(f.folderId) : undefined,
},
)));
return items.filter(x => x != null);
}

View File

@@ -12,6 +12,9 @@ import type { } from '@/models/Blocking.js';
import type { MiDriveFolder } from '@/models/DriveFolder.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { In } from 'typeorm';
import { uniqueByKey } from '@/misc/unique-by-key.js';
import { splitIdAndObjects } from '@/misc/split-id-and-objects.js';
@Injectable()
export class DriveFolderEntityService {
@@ -32,12 +35,20 @@ export class DriveFolderEntityService {
options?: {
detail: boolean
},
hint?: {
folderMap?: Map<string, MiDriveFolder>;
foldersCountMap?: Map<string, number> | null;
filesCountMap?: Map<string, number> | null;
parentPacker?: (id: string) => Promise<Packed<'DriveFolder'>>;
},
): Promise<Packed<'DriveFolder'>> {
const opts = Object.assign({
detail: false,
}, options);
const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
const folder = typeof src === 'object'
? src
: hint?.folderMap?.get(src) ?? await this.driveFoldersRepository.findOneByOrFail({ id: src });
return await awaitAll({
id: folder.id,
@@ -46,20 +57,141 @@ export class DriveFolderEntityService {
parentId: folder.parentId,
...(opts.detail ? {
foldersCount: this.driveFoldersRepository.countBy({
parentId: folder.id,
}),
filesCount: this.driveFilesRepository.countBy({
folderId: folder.id,
}),
foldersCount: hint?.foldersCountMap?.get(folder.id)
?? this.driveFoldersRepository.countBy({
parentId: folder.id,
}),
filesCount: hint?.filesCountMap?.get(folder.id)
?? this.driveFilesRepository.countBy({
folderId: folder.id,
}),
...(folder.parentId ? {
parent: this.pack(folder.parentId, {
detail: true,
}),
parent: hint?.parentPacker
? hint.parentPacker(folder.parentId)
: this.pack(folder.parentId, { detail: true }, hint),
} : {}),
} : {}),
});
}
}
public async packMany(
src: Array<MiDriveFolder['id'] | MiDriveFolder>,
options?: {
detail: boolean
},
): Promise<Array<Packed<'DriveFolder'>>> {
/**
* 重複を除去しつつ、必要なDriveFolderオブジェクトをすべて取得する
*/
const collectUniqueObjects = async (src: Array<MiDriveFolder['id'] | MiDriveFolder>) => {
const uniqueSrc = uniqueByKey(
src,
(s) => typeof s === 'string' ? s : s.id,
);
const { ids, objects } = splitIdAndObjects(uniqueSrc);
const uniqueObjects = new Map<string, MiDriveFolder>(objects.map(s => [s.id, s]));
const needsFetchIds = ids.filter(id => !uniqueObjects.has(id));
if (needsFetchIds.length > 0) {
const fetchedObjects = await this.driveFoldersRepository.find({
where: {
id: In(needsFetchIds),
},
});
for (const obj of fetchedObjects) {
uniqueObjects.set(obj.id, obj);
}
}
return uniqueObjects;
};
/**
* 親フォルダーを再帰的に収集する
*/
const collectAncestors = async (folderMap: Map<string, MiDriveFolder>) => {
for (;;) {
const parentIds = new Set<string>();
for (const folder of folderMap.values()) {
if (folder.parentId != null && !folderMap.has(folder.parentId)) {
parentIds.add(folder.parentId);
}
}
if (parentIds.size === 0) break;
const fetchedParents = await this.driveFoldersRepository.find({
where: {
id: In([...parentIds]),
},
});
if (fetchedParents.length === 0) break;
for (const parent of fetchedParents) {
folderMap.set(parent.id, parent);
}
}
};
const opts = Object.assign({
detail: false,
}, options);
const folderMap = await collectUniqueObjects(src);
let foldersCountMap: Map<string, number> | null = null;
let filesCountMap: Map<string, number> | null = null;
if (opts.detail) {
await collectAncestors(folderMap);
const ids = [...folderMap.keys()];
if (ids.length > 0) {
const folderCounts = await this.driveFoldersRepository.createQueryBuilder('folder')
.select('folder.parentId', 'parentId')
.addSelect('COUNT(*)', 'count')
.where('folder.parentId IN (:...ids)', { ids })
.groupBy('folder.parentId')
.getRawMany<{ parentId: string; count: string }>();
const fileCounts = await this.driveFilesRepository.createQueryBuilder('file')
.select('file.folderId', 'folderId')
.addSelect('COUNT(*)', 'count')
.where('file.folderId IN (:...ids)', { ids })
.groupBy('file.folderId')
.getRawMany<{ folderId: string; count: string }>();
foldersCountMap = new Map(folderCounts.map(row => [row.parentId, Number(row.count)]));
filesCountMap = new Map(fileCounts.map(row => [row.folderId, Number(row.count)]));
} else {
foldersCountMap = new Map();
filesCountMap = new Map();
}
}
const packedMap = new Map<string, Promise<Packed<'DriveFolder'>>>();
const packFromId = (id: string): Promise<Packed<'DriveFolder'>> => {
const cached = packedMap.get(id);
if (cached) return cached;
const folder = folderMap.get(id);
if (!folder) {
throw new Error(`DriveFolder not found: ${id}`);
}
const packedPromise = this.pack(folder, options, {
folderMap,
foldersCountMap,
filesCountMap,
parentPacker: packFromId,
});
packedMap.set(id, packedPromise);
return packedPromise;
};
return Promise.all(src.map(s => packFromId(typeof s === 'string' ? s : s.id)));
}
}

View File

@@ -41,7 +41,7 @@ export class EmojiEntityService {
@bindThis
public packSimpleMany(
emojis: any[],
emojis: (MiEmoji['id'] | MiEmoji)[],
) {
return Promise.all(emojis.map(x => this.packSimple(x)));
}
@@ -69,7 +69,7 @@ export class EmojiEntityService {
@bindThis
public packDetailedMany(
emojis: any[],
emojis: (MiEmoji['id'] | MiEmoji)[],
): Promise<Packed<'EmojiDetailed'>[]> {
return Promise.all(emojis.map(x => this.packDetailed(x)));
}

View File

@@ -55,13 +55,13 @@ export class MetaEntityService {
if (instance.defaultLightTheme) {
try {
defaultLightTheme = JSON.stringify(JSON5.parse(instance.defaultLightTheme));
} catch (e) {
} catch (_) {
}
}
if (instance.defaultDarkTheme) {
try {
defaultDarkTheme = JSON.stringify(JSON5.parse(instance.defaultDarkTheme));
} catch (e) {
} catch (_) {
}
}

View File

@@ -17,6 +17,7 @@ import { DebounceLoader } from '@/misc/loader.js';
import { IdService } from '@/core/IdService.js';
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
import { CacheService } from '@/core/CacheService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { ReactionService } from '../ReactionService.js';
@@ -66,6 +67,7 @@ export class NoteEntityService implements OnModuleInit {
private reactionService: ReactionService;
private reactionsBufferingService: ReactionsBufferingService;
private idService: IdService;
private cacheService: CacheService;
private noteLoader = new DebounceLoader(this.findNoteOrFail);
constructor(
@@ -101,6 +103,7 @@ export class NoteEntityService implements OnModuleInit {
//private reactionService: ReactionService,
//private reactionsBufferingService: ReactionsBufferingService,
//private idService: IdService,
//private cacheService: CacheService,
) {
}
@@ -111,6 +114,7 @@ export class NoteEntityService implements OnModuleInit {
this.reactionService = this.moduleRef.get('ReactionService');
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
this.idService = this.moduleRef.get('IdService');
this.cacheService = this.moduleRef.get('CacheService');
}
@bindThis
@@ -125,75 +129,65 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
if (meId === packedNote.userId) return;
public async shouldHideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<boolean> {
if (meId === packedNote.userId) return false;
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
let hide = false;
if (packedNote.user.requireSigninToViewContents && meId == null) {
hide = true;
return true;
}
if (!hide) {
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
hide = true;
}
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
return true;
}
// visibility が specified かつ自分が指定されていなかったら非表示
if (!hide) {
if (packedNote.visibility === 'specified') {
if (meId == null) {
hide = true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (packedNote.visibility === 'specified') {
if (meId == null) {
return true;
} else {
// 指定されているかどうか
const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (!specified) {
hide = true;
}
if (!specified) {
return true;
}
}
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (!hide) {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,
},
});
hide = !isFollowing;
if (packedNote.visibility === 'followers') {
if (meId == null) {
return true;
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
return false;
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
// 自分へのメンション
return false;
} else {
// フォロワーかどうか
const followings = await this.cacheService.userFollowingsCache.fetch(meId);
if (!Object.hasOwn(followings, packedNote.userId)) {
return true;
}
}
}
if (hide) {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
return false;
}
@bindThis
public hideNote(packedNote: Packed<'Note'>): void {
packedNote.visibleUserIds = undefined;
packedNote.fileIds = [];
packedNote.files = [];
packedNote.text = null;
packedNote.poll = undefined;
packedNote.cw = null;
packedNote.isHidden = true;
// TODO: hiddenReason みたいなのを提供しても良さそう
}
@bindThis
@@ -278,7 +272,7 @@ export class NoteEntityService implements OnModuleInit {
@bindThis
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
// This code must always be synchronized with the checks in generateVisibilityQuery.
// This code must always be synchronized with the checks in QueryService.generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === 'specified') {
if (meId == null) {
@@ -468,8 +462,8 @@ export class NoteEntityService implements OnModuleInit {
this.treatVisibility(packed);
if (!opts.skipHide) {
await this.hideNote(packed, meId);
if (!opts.skipHide && await this.shouldHideNote(packed, meId)) {
this.hideNote(packed);
}
return packed;

View File

@@ -54,7 +54,7 @@ export class NoteReactionEntityService implements OnModuleInit {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
const _opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });
@@ -90,7 +90,7 @@ export class NoteReactionEntityService implements OnModuleInit {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReactionWithNote'>> {
const opts = Object.assign({
const _opts = Object.assign({
}, options);
const reaction = typeof src === 'object' ? src : await this.noteReactionsRepository.findOneByOrFail({ id: src });

View File

@@ -14,6 +14,10 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from './UserEntityService.js';
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
return ['random', '1', '2'].includes(bw);
}
@Injectable()
export class ReversiGameEntityService {
constructor(
@@ -58,7 +62,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,
@@ -116,7 +120,7 @@ export class ReversiGameEntityService {
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
bw: game.bw,
bw: assertBw(game.bw) ? game.bw : 'random',
isLlotheo: game.isLlotheo,
canPutEverywhere: game.canPutEverywhere,
loopedBoard: game.loopedBoard,

View File

@@ -720,7 +720,7 @@ export class UserEntityService implements OnModuleInit {
me,
{
...options,
userProfile: profilesMap.get(u.id),
userProfile: profilesMap?.get(u.id),
userRelations: userRelations,
userMemos: userMemos,
pinNotes: pinNotes,

View File

@@ -4,13 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import si from 'systeminformation';
import Xev from 'xev';
import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev();
@@ -97,12 +96,14 @@ function cpuUsage(): Promise<number> {
// MEMORY STAT
async function mem() {
const si = await import('systeminformation');
const data = await si.mem();
return data;
}
// NETWORK STAT
async function net() {
const si = await import('systeminformation');
const iface = await si.networkInterfaceDefault();
const data = await si.networkStats(iface);
return data[0];
@@ -110,5 +111,6 @@ async function net() {
// FS STAT
async function fs() {
const si = await import('systeminformation');
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
}

View File

@@ -56,7 +56,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
try {
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
} catch (_) {
// This should never happen due to input sanitisation.
return false;
}

View File

@@ -12,7 +12,7 @@ export function getIpHash(ip: string): string {
// (this means for IPv4 the entire address is used)
const prefix = IPCIDR.createAddress(ip).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
} catch (e) {
} catch (_) {
const prefix = IPCIDR.createAddress(ip.replace(/:[0-9]+$/, '')).mask(64);
return 'ip-' + BigInt('0b' + prefix).toString(36);
}

View File

@@ -26,7 +26,7 @@ export class I18n<T extends Record<string, any>> {
}
}
return str;
} catch (e) {
} catch (_) {
console.warn(`missing localization '${key}'`);
return key;
}

View File

@@ -64,6 +64,7 @@ import {
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
packedMetaLiteSchema,
packedMetaClientOptionsSchema,
} from '@/models/json-schema/meta.js';
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
@@ -135,6 +136,7 @@ export const refs = {
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
MetaClientOptions: packedMetaClientOptionsSchema,
UserWebhook: packedUserWebhookSchema,
SystemWebhook: packedSystemWebhookSchema,
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
@@ -262,8 +264,6 @@ type ObjectSchemaTypeDef<p extends Schema> =
never :
any;
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
export type SchemaTypeDef<p extends Schema> =
p['type'] extends 'null' ? null :
p['type'] extends 'integer' ? number :

View File

@@ -4,15 +4,11 @@
*/
import * as os from 'node:os';
import sysUtils from 'systeminformation';
import type Logger from '@/logger.js';
export async function showMachineInfo(parentLogger: Logger) {
const logger = parentLogger.createSubLogger('machine');
logger.debug(`Hostname: ${os.hostname()}`);
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
const mem = await sysUtils.mem();
const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
logger.debug(`CPU: ${os.cpus().length} core MEM: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB (available: ${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)}GB)`);
}

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* idとオブジェクトを分離する
* @param input idまたはオブジェクトの配列
* @returns idの配列とオブジェクトの配列
*/
export function splitIdAndObjects<T extends { id: string }>(input: (T | string)[]): { ids: string[]; objects: T[] } {
const ids: string[] = [];
const objects : T[] = [];
for (const item of input) {
if (typeof item === 'string') {
ids.push(item);
} else {
objects.push(item);
}
}
return {
ids,
objects,
};
}

View File

@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* itemsの中でkey関数が返す値が重複しないようにした配列を返す
* @param items 重複を除去したい配列
* @param key 重複判定に使うキーを返す関数
* @returns 重複を除去した配列
*/
export function uniqueByKey<TItem, TKey = string>(items: Iterable<TItem>, key: (item: TItem) => TKey): TItem[] {
const map = new Map<TKey, TItem>();
for (const item of items) {
const k = key(item);
if (!map.has(k)) {
map.set(k, item);
}
}
return [...map.values()];
}

View File

@@ -67,7 +67,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザ.
*/
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
@@ -76,7 +76,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のユーザプロフィール.
*/
@ManyToOne(type => MiUserProfile, {
@ManyToOne(() => MiUserProfile, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
@@ -96,7 +96,7 @@ export class MiAbuseReportNotificationRecipient {
/**
* 通知先のシステムWebhook.
*/
@ManyToOne(type => MiSystemWebhook, {
@ManyToOne(() => MiSystemWebhook, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'systemWebhookId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_systemWebhookId' })

View File

@@ -18,7 +18,7 @@ export class MiAbuseUserReport {
@Column(id())
public targetUserId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAbuseUserReport {
@Column(id())
public reporterId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiAbuseUserReport {
})
public assigneeId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -41,7 +41,7 @@ export class MiAccessToken {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -53,7 +53,7 @@ export class MiAccessToken {
})
public appId: MiApp['id'] | null;
@ManyToOne(type => MiApp, {
@ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -79,7 +79,7 @@ export class MiAnnouncement {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiAnnouncementRead {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiAnnouncementRead {
@Column(id())
public announcementId: MiAnnouncement['id'];
@ManyToOne(type => MiAnnouncement, {
@ManyToOne(() => MiAnnouncement, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -24,7 +24,7 @@ export class MiAntenna {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -45,7 +45,7 @@ export class MiAntenna {
})
public userListId: MiUserList['id'] | null;
@ManyToOne(type => MiUserList, {
@ManyToOne(() => MiUserList, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiApp {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})

View File

@@ -25,7 +25,7 @@ export class MiAuthSession {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
nullable: true,
})
@@ -35,7 +35,7 @@ export class MiAuthSession {
@Column(id())
public appId: MiApp['id'];
@ManyToOne(type => MiApp, {
@ManyToOne(() => MiApp, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiBlocking {
})
public blockeeId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiBlocking {
})
public blockerId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiBubbleGameRecord {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -27,7 +27,7 @@ export class MiChannel {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -52,7 +52,7 @@ export class MiChannel {
})
public bannerId: MiDriveFile['id'] | null;
@ManyToOne(type => MiDriveFile, {
@ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiChannelFavorite {
})
public channelId: MiChannel['id'];
@ManyToOne(type => MiChannel, {
@ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChannelFavorite {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -21,7 +21,7 @@ export class MiChannelFollowing {
})
public followeeId: MiChannel['id'];
@ManyToOne(type => MiChannel, {
@ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiChannelFollowing {
})
public followerId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiChannelMuting {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChannelMuting {
})
public channelId: MiChannel['id'];
@ManyToOne(type => MiChannel, {
@ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -19,7 +19,7 @@ export class MiChatApproval {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -31,7 +31,7 @@ export class MiChatApproval {
})
public otherId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiChatMessage {
})
public fromUserId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatMessage {
})
public toUserId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -44,7 +44,7 @@ export class MiChatMessage {
})
public toRoomId: MiChatRoom['id'] | null;
@ManyToOne(type => MiChatRoom, {
@ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -72,7 +72,7 @@ export class MiChatMessage {
})
public fileId: MiDriveFile['id'] | null;
@ManyToOne(type => MiDriveFile, {
@ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -23,7 +23,7 @@ export class MiChatRoom {
})
public ownerId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiChatRoomInvitation {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomInvitation {
})
public roomId: MiChatRoom['id'];
@ManyToOne(type => MiChatRoom, {
@ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiChatRoomMembership {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -32,7 +32,7 @@ export class MiChatRoomMembership {
})
public roomId: MiChatRoom['id'];
@ManyToOne(type => MiChatRoom, {
@ManyToOne(() => MiChatRoom, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -25,7 +25,7 @@ export class MiClip {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiClipFavorite {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiClipFavorite {
@Column(id())
public clipId: MiClip['id'];
@ManyToOne(type => MiClip, {
@ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -21,7 +21,7 @@ export class MiClipNote {
})
public noteId: MiNote['id'];
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiClipNote {
})
public clipId: MiClip['id'];
@ManyToOne(type => MiClip, {
@ManyToOne(() => MiClip, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -22,7 +22,7 @@ export class MiDriveFile {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
})
@JoinColumn()
@@ -142,7 +142,7 @@ export class MiDriveFile {
})
public folderId: MiDriveFolder['id'] | null;
@ManyToOne(type => MiDriveFolder, {
@ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -26,7 +26,7 @@ export class MiDriveFolder {
})
public userId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -40,7 +40,7 @@ export class MiDriveFolder {
})
public parentId: MiDriveFolder['id'] | null;
@ManyToOne(type => MiDriveFolder, {
@ManyToOne(() => MiDriveFolder, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -38,7 +38,7 @@ export class MiFlash {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiFlashLike {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiFlashLike {
@Column(id())
public flashId: MiFlash['id'];
@ManyToOne(type => MiFlash, {
@ManyToOne(() => MiFlash, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -20,7 +20,7 @@ export class MiFollowRequest {
})
public followeeId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -33,7 +33,7 @@ export class MiFollowRequest {
})
public followerId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -21,7 +21,7 @@ export class MiFollowing {
})
public followeeId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -34,7 +34,7 @@ export class MiFollowing {
})
public followerId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiGalleryLike {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiGalleryLike {
@Column(id())
public postId: MiGalleryPost['id'];
@ManyToOne(type => MiGalleryPost, {
@ManyToOne(() => MiGalleryPost, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -36,7 +36,7 @@ export class MiGalleryPost {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -21,7 +21,7 @@ export class MiMeta {
})
public rootUserId: MiUser['id'] | null;
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'SET NULL',
nullable: true,
})
@@ -725,7 +725,11 @@ export class MiMeta {
@Column('jsonb', {
default: { },
})
public clientOptions: Record<string, any>;
public clientOptions: {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};
}
export type SoftwareSuspension = {

View File

@@ -16,7 +16,7 @@ export class MiModerationLog {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -32,7 +32,7 @@ export class MiMuting {
})
public muteeId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -45,7 +45,7 @@ export class MiMuting {
})
public muterId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -35,7 +35,7 @@ export class MiNote {
})
public replyId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -49,7 +49,7 @@ export class MiNote {
})
public renoteId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -83,7 +83,7 @@ export class MiNote {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -208,7 +208,7 @@ export class MiNote {
})
public channelId: MiChannel['id'] | null;
@ManyToOne(type => MiChannel, {
@ManyToOne(() => MiChannel, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -27,7 +27,7 @@ export class MiNoteDraft {
public replyId: MiNote['id'] | null;
// There is a possibility that replyId is not null but reply is null when the reply note is deleted.
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -42,7 +42,7 @@ export class MiNoteDraft {
public renoteId: MiNote['id'] | null;
// There is a possibility that renoteId is not null but renote is null when the renote note is deleted.
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
createForeignKeyConstraints: false,
})
@JoinColumn()
@@ -66,7 +66,7 @@ export class MiNoteDraft {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -120,7 +120,7 @@ export class MiNoteDraft {
// There is a possibility that channelId is not null but channel is null when the channel is deleted.
// (deleting channel is not implemented so it's not happening now but may happen in the future)
@ManyToOne(type => MiChannel, {
@ManyToOne(() => MiChannel, {
createForeignKeyConstraints: false,
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiNoteFavorite {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiNoteFavorite {
@Column(id())
public noteId: MiNote['id'];
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiNoteReaction {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -28,7 +28,7 @@ export class MiNoteReaction {
@Column(id())
public noteId: MiNote['id'];
@ManyToOne(type => MiNote, {
@ManyToOne(() => MiNote, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -19,7 +19,7 @@ export class MiNoteThreadMuting {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -56,7 +56,7 @@ export class MiPage {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -68,7 +68,7 @@ export class MiPage {
})
public eyeCatchingImageId: MiDriveFile['id'] | null;
@ManyToOne(type => MiDriveFile, {
@ManyToOne(() => MiDriveFile, {
onDelete: 'SET NULL',
})
@JoinColumn()

View File

@@ -18,7 +18,7 @@ export class MiPageLike {
@Column(id())
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()
@@ -27,7 +27,7 @@ export class MiPageLike {
@Column(id())
public pageId: MiPage['id'];
@ManyToOne(type => MiPage, {
@ManyToOne(() => MiPage, {
onDelete: 'CASCADE',
})
@JoinColumn()

View File

@@ -24,7 +24,7 @@ export class MiPasswordResetRequest {
})
public userId: MiUser['id'];
@ManyToOne(type => MiUser, {
@ManyToOne(() => MiUser, {
onDelete: 'CASCADE',
})
@JoinColumn()

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