mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-25 02:44:03 +02:00
Merge remote-tracking branch 'msky/develop' into copilot/add-user-mute-settings
This commit is contained in:
@@ -3,14 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js";
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
|
||||
export class CompositeNoteIndex1745378064470 {
|
||||
name = 'CompositeNoteIndex1745378064470';
|
||||
transaction = isConcurrentIndexMigrationEnabled() ? false : undefined;
|
||||
transaction = isConcurrentIndexMigrationEnabled ? false : undefined;
|
||||
|
||||
async up(queryRunner) {
|
||||
const concurrently = isConcurrentIndexMigrationEnabled();
|
||||
const concurrently = isConcurrentIndexMigrationEnabled;
|
||||
|
||||
if (concurrently) {
|
||||
const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
|
||||
@@ -29,7 +29,7 @@ export class CompositeNoteIndex1745378064470 {
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
|
||||
const mayConcurrently = isConcurrentIndexMigrationEnabled ? 'CONCURRENTLY' : '';
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
|
||||
await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import {loadConfig} from "./js/migration-config.js";
|
||||
|
||||
export class MigrateSomeConfigFileSettingsToMeta1746949539915 {
|
||||
name = 'MigrateSomeConfigFileSettingsToMeta1746949539915'
|
||||
|
||||
async up(queryRunner) {
|
||||
const config = loadConfig();
|
||||
// $1 cannot be used in ALTER TABLE queries
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT ${config.proxyRemoteFiles}`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT ${config.signToActivityPubGet}`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT ${!config.disallowExternalApRedirect}`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "proxyRemoteFiles" boolean NOT NULL DEFAULT TRUE`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "signToActivityPubGet" boolean NOT NULL DEFAULT TRUE`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "allowExternalApRedirect" boolean NOT NULL DEFAULT TRUE`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { path as configYamlPath } from '../../built/config.js';
|
||||
import * as yaml from 'js-yaml';
|
||||
import fs from "node:fs";
|
||||
|
||||
export function isConcurrentIndexMigrationEnabled() {
|
||||
return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
}
|
||||
|
||||
let loadedConfigCache = undefined;
|
||||
|
||||
function loadConfigInternal() {
|
||||
const config = yaml.load(fs.readFileSync(configYamlPath, 'utf-8'));
|
||||
|
||||
return {
|
||||
disallowExternalApRedirect: Boolean(config.disallowExternalApRedirect ?? false),
|
||||
proxyRemoteFiles: Boolean(config.proxyRemoteFiles ?? false),
|
||||
signToActivityPubGet: Boolean(config.signToActivityPubGet ?? true),
|
||||
}
|
||||
}
|
||||
|
||||
export function loadConfig() {
|
||||
if (loadedConfigCache === undefined) {
|
||||
loadedConfigCache = loadConfigInternal();
|
||||
}
|
||||
return loadedConfigCache;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { entities } from './built/postgres.js';
|
||||
import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js";
|
||||
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
@@ -15,5 +16,5 @@ export default new DataSource({
|
||||
extra: config.db.extra,
|
||||
entities: entities,
|
||||
migrations: ['migration/*.js'],
|
||||
migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all',
|
||||
migrationsTransactionMode: isConcurrentIndexMigrationEnabled ? 'each' : 'all',
|
||||
});
|
||||
|
||||
@@ -7,36 +7,37 @@
|
||||
"node": "^22.15.0 || ^24.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
"start:inspect": "node --inspect ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"cli": "node ./built/boot/cli.js",
|
||||
"check:connect": "node ./scripts/check_connect.js",
|
||||
"start": "pnpm compile-config && node ./built/boot/entry.js",
|
||||
"start:inspect": "pnpm compile-config && node --inspect ./built/boot/entry.js",
|
||||
"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",
|
||||
"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: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",
|
||||
"watch": "node ./scripts/watch.mjs",
|
||||
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
|
||||
"restart": "pnpm build && pnpm start",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"dev": "pnpm compile-config && node ./scripts/dev.mjs",
|
||||
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
||||
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache",
|
||||
"jest": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "pnpm compile-config && node ./jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test:fed": "pnpm jest:fed",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"check-migrations": "node scripts/check_migrations_clean.js",
|
||||
"generate-api-json": "node ./scripts/generate_api_json.js"
|
||||
"generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
@@ -70,26 +71,25 @@
|
||||
"utf-8-validate": "6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.937.0",
|
||||
"@aws-sdk/lib-storage": "3.937.0",
|
||||
"@aws-sdk/client-s3": "3.947.0",
|
||||
"@aws-sdk/lib-storage": "3.947.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.3",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.1.0",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "11.3.0",
|
||||
"@fastify/http-proxy": "11.4.1",
|
||||
"@fastify/multipart": "9.3.0",
|
||||
"@fastify/static": "8.3.0",
|
||||
"@kitajs/html": "4.2.11",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.82",
|
||||
"@napi-rs/canvas": "0.1.84",
|
||||
"@nestjs/common": "11.1.9",
|
||||
"@nestjs/core": "11.1.9",
|
||||
"@nestjs/testing": "11.1.9",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.26.0",
|
||||
"@sentry/profiling-node": "10.26.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",
|
||||
@@ -103,13 +103,12 @@
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "2.2.0",
|
||||
"bullmq": "5.64.1",
|
||||
"body-parser": "2.2.1",
|
||||
"bullmq": "5.65.1",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "10.0.11",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
"chokidar": "4.0.3",
|
||||
"chokidar": "5.0.0",
|
||||
"color-convert": "3.1.3",
|
||||
"content-disposition": "1.0.1",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -120,18 +119,16 @@
|
||||
"file-type": "21.1.1",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.5",
|
||||
"got": "14.6.4",
|
||||
"got": "14.6.5",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
"ioredis": "5.8.2",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"ipaddr.js": "2.3.0",
|
||||
"is-svg": "6.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "9.0.0",
|
||||
"jsrsasign": "11.1.0",
|
||||
"juice": "11.0.3",
|
||||
"meilisearch": "0.54.0",
|
||||
"mfm-js": "0.25.0",
|
||||
@@ -143,15 +140,14 @@
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.0.1",
|
||||
"nodemailer": "7.0.10",
|
||||
"nodemailer": "7.0.11",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth": "0.10.2",
|
||||
"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.0",
|
||||
"pkce-challenge": "5.0.1",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
"qrcode": "1.5.4",
|
||||
@@ -170,14 +166,13 @@
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.27.11",
|
||||
"systeminformation": "5.27.12",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.27",
|
||||
"typeorm": "0.3.28",
|
||||
"typescript": "5.9.3",
|
||||
"ulid": "3.0.1",
|
||||
"ulid": "3.0.2",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.18.3",
|
||||
@@ -187,7 +182,7 @@
|
||||
"@jest/globals": "29.7.0",
|
||||
"@kitajs/ts-html-plugin": "4.1.3",
|
||||
"@nestjs/platform-express": "11.1.9",
|
||||
"@sentry/vue": "10.26.0",
|
||||
"@sentry/vue": "10.29.0",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.39",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -198,14 +193,11 @@
|
||||
"@types/fluent-ffmpeg": "2.1.28",
|
||||
"@types/http-link-header": "1.0.7",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/jsrsasign": "10.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"@types/oauth": "0.9.6",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.15.6",
|
||||
@@ -223,20 +215,21 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor": "10.0.11",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"jest-util": "29.7.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"nodemon": "3.1.11",
|
||||
"pid-port": "2.0.0",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.1.4",
|
||||
"vite": "7.2.4"
|
||||
"vite": "7.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
54
packages/backend/scripts/compile_config.js
Normal file
54
packages/backend/scripts/compile_config.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* YAMLファイルをJSONファイルに変換するスクリプト
|
||||
* ビルド前に実行し、ランタイムにjs-yamlを含まないようにする
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const configDir = resolve(_dirname, '../../../.config');
|
||||
const OUTPUT_PATH = resolve(_dirname, '../../../built/.config.json');
|
||||
|
||||
// TODO: yamlのパースに失敗したときのエラーハンドリング
|
||||
|
||||
/**
|
||||
* YAMLファイルをJSONファイルに変換
|
||||
* @param {string} ymlPath - YAMLファイルのパス
|
||||
*/
|
||||
function yamlToJson(ymlPath) {
|
||||
if (!fs.existsSync(ymlPath)) {
|
||||
console.warn(`YAML file not found: ${ymlPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${ymlPath} → ${OUTPUT_PATH}`);
|
||||
|
||||
const yamlContent = fs.readFileSync(ymlPath, 'utf-8');
|
||||
const jsonContent = yaml.load(yamlContent);
|
||||
if (!fs.existsSync(dirname(OUTPUT_PATH))) {
|
||||
fs.mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(OUTPUT_PATH, JSON.stringify({
|
||||
'_NOTE_': 'This file is auto-generated from YAML file. DO NOT EDIT.',
|
||||
...jsonContent,
|
||||
}), 'utf-8');
|
||||
}
|
||||
|
||||
if (process.env.MISSKEY_CONFIG_YML) {
|
||||
const customYmlPath = resolve(configDir, process.env.MISSKEY_CONFIG_YML);
|
||||
yamlToJson(customYmlPath);
|
||||
} else {
|
||||
yamlToJson(resolve(configDir, process.env.NODE_ENV === 'test' ? 'test.yml' : 'default.yml'));
|
||||
}
|
||||
|
||||
console.log('Configuration compiled ✓');
|
||||
152
packages/backend/scripts/measure-memory.mjs
Normal file
152
packages/backend/scripts/measure-memory.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* This script starts the Misskey backend server, waits for it to be ready,
|
||||
* measures memory usage, and outputs the result as JSON.
|
||||
*
|
||||
* Usage: node scripts/measure-memory.mjs
|
||||
*/
|
||||
|
||||
import { fork } from 'node:child_process';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
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();
|
||||
|
||||
// Start the Misskey backend server using fork to enable IPC
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
||||
cwd: join(__dirname, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
});
|
||||
|
||||
let serverReady = false;
|
||||
|
||||
// Listen for the 'ok' message from the server indicating it's ready
|
||||
serverProcess.on('message', (message) => {
|
||||
if (message === 'ok') {
|
||||
serverReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle server output
|
||||
serverProcess.stdout?.on('data', (data) => {
|
||||
process.stderr.write(`[server stdout] ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr?.on('data', (data) => {
|
||||
process.stderr.write(`[server stderr] ${data}`);
|
||||
});
|
||||
|
||||
// Handle server error
|
||||
serverProcess.on('error', (err) => {
|
||||
process.stderr.write(`[server error] ${err}\n`);
|
||||
});
|
||||
|
||||
// Wait for server to be ready or timeout
|
||||
const startupStartTime = Date.now();
|
||||
while (!serverReady) {
|
||||
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
|
||||
serverProcess.kill('SIGTERM');
|
||||
throw new Error('Server startup timeout');
|
||||
}
|
||||
await setTimeout(100);
|
||||
}
|
||||
|
||||
const startupTime = Date.now() - startupStartTime;
|
||||
process.stderr.write(`Server started in ${startupTime}ms\n`);
|
||||
|
||||
// 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');
|
||||
|
||||
// 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/);
|
||||
|
||||
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 { 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
serverProcess.kill('SIGTERM');
|
||||
|
||||
// Wait for process to exit
|
||||
let exited = false;
|
||||
await new Promise((resolve) => {
|
||||
serverProcess.on('exit', () => {
|
||||
exited = true;
|
||||
resolve(undefined);
|
||||
});
|
||||
// Force kill after 10 seconds if not exited
|
||||
setTimeout(10000).then(() => {
|
||||
if (!exited) {
|
||||
serverProcess.kill('SIGKILL');
|
||||
}
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
startupTimeMs: startupTime,
|
||||
memory: memoryInfo,
|
||||
};
|
||||
|
||||
// Output as JSON to stdout
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
measureMemory().catch((err) => {
|
||||
console.error(JSON.stringify({
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,7 +6,6 @@
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import { type FastifyServerOptions } from 'fastify';
|
||||
import type * as Sentry from '@sentry/node';
|
||||
import type * as SentryVue from '@sentry/vue';
|
||||
@@ -218,21 +217,15 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
/**
|
||||
* Path of configuration directory
|
||||
*/
|
||||
const dir = `${_dirname}/../../../.config`;
|
||||
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
|
||||
|
||||
/**
|
||||
* Path of configuration file
|
||||
*/
|
||||
export const path = process.env.MISSKEY_CONFIG_YML
|
||||
? resolve(dir, process.env.MISSKEY_CONFIG_YML)
|
||||
: process.env.NODE_ENV === 'test'
|
||||
? resolve(dir, 'test.yml')
|
||||
: resolve(dir, 'default.yml');
|
||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.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 frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||
@@ -244,7 +237,7 @@ export function loadConfig(): Config {
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||
: { 'src/boot.ts': { file: null } };
|
||||
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||
|
||||
const url = tryCreateUrl(config.url ?? process.env.MISSKEY_URL ?? '');
|
||||
const version = meta.version;
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos
|
||||
import { bindThis } from '@/decorators.js';
|
||||
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 type { OnModuleInit } from '@nestjs/common';
|
||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||
@@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
|
||||
if ((followersOnlyBefore != null)
|
||||
&& (
|
||||
(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
|
||||
|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
|
||||
)
|
||||
) {
|
||||
if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) {
|
||||
packedNote.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
@@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
|
||||
if (!hide) {
|
||||
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
|
||||
if ((hiddenBefore != null)
|
||||
&& (
|
||||
(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
|
||||
|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
|
||||
)
|
||||
) {
|
||||
if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/backend/src/misc/should-hide-note-by-time.ts
Normal file
29
packages/backend/src/misc/should-hide-note-by-time.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* ノートが指定された時間条件に基づいて非表示対象かどうかを判定する
|
||||
* @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない)
|
||||
* @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト)
|
||||
* @returns 非表示にすべき場合は true
|
||||
*/
|
||||
export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean {
|
||||
if (hiddenBefore == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime();
|
||||
|
||||
if (hiddenBefore <= 0) {
|
||||
// 負の値: 作成からの経過時間(秒)で判定
|
||||
const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
|
||||
const hideAfterSeconds = Math.abs(hiddenBefore);
|
||||
return elapsedSeconds >= hideAfterSeconds;
|
||||
} else {
|
||||
// 正の値: 絶対的なタイムスタンプ(秒)で判定
|
||||
const createdAtSeconds = createdAtTime / 1000;
|
||||
return createdAtSeconds <= hiddenBefore;
|
||||
}
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
let Sentry: typeof import('@sentry/node') | undefined;
|
||||
if (Sentry != null) {
|
||||
if (this.config.sentryForBackend) {
|
||||
import('@sentry/node').then((mod) => {
|
||||
Sentry = mod;
|
||||
});
|
||||
|
||||
@@ -5,21 +5,20 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Writable } from 'node:stream';
|
||||
import { Inject, Injectable, StreamableFile } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, 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 { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
@@ -43,6 +42,7 @@ export class ExportClipsProcessorService {
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
@@ -100,16 +100,16 @@ export class ExportClipsProcessorService {
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const clips = await this.clipsRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
const query = this.clipsRepository.createQueryBuilder('clip')
|
||||
.where('clip.userId = :userId', { userId: user.id })
|
||||
.orderBy('clip.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('clip.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
const clips = await query.getMany();
|
||||
|
||||
if (clips.length === 0) {
|
||||
job.updateProgress(100);
|
||||
@@ -124,7 +124,7 @@ export class ExportClipsProcessorService {
|
||||
const isFirst = exportedClipsCount === 0;
|
||||
await writer.write(isFirst ? content : ',\n' + content);
|
||||
|
||||
await this.processClipNotes(writer, clip.id);
|
||||
await this.processClipNotes(writer, clip.id, user.id);
|
||||
|
||||
await writer.write(']}');
|
||||
exportedClipsCount++;
|
||||
@@ -134,22 +134,25 @@ export class ExportClipsProcessorService {
|
||||
}
|
||||
}
|
||||
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
|
||||
async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: 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 } })[];
|
||||
const query = this.clipNotesRepository.createQueryBuilder('clipNote')
|
||||
.leftJoinAndSelect('clipNote.note', 'note')
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.where('clipNote.clipId = :clipId', { clipId })
|
||||
.orderBy('clipNote.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('clipNote.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, { id: userId });
|
||||
|
||||
const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (clipNotes.length === 0) {
|
||||
break;
|
||||
@@ -158,6 +161,11 @@ export class ExportClipsProcessorService {
|
||||
cursor = clipNotes.at(-1)?.id ?? null;
|
||||
|
||||
for (const clipNote of clipNotes) {
|
||||
const noteCreatedAt = this.idService.parse(clipNote.note.id).date;
|
||||
if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let poll: MiPoll | undefined;
|
||||
if (clipNote.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js';
|
||||
@@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbJobDataWithUser } from '../types.js';
|
||||
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
@@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService {
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const favorites = await this.noteFavoritesRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
relations: ['note', 'note.user'],
|
||||
}) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
|
||||
const query = this.noteFavoritesRepository.createQueryBuilder('favorite')
|
||||
.leftJoinAndSelect('favorite.note', 'note')
|
||||
.leftJoinAndSelect('note.user', 'user')
|
||||
.where('favorite.userId = :userId', { userId: user.id })
|
||||
.orderBy('favorite.id', 'ASC')
|
||||
.take(100);
|
||||
|
||||
if (cursor) {
|
||||
query.andWhere('favorite.id > :cursor', { cursor });
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, { id: user.id });
|
||||
|
||||
const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
|
||||
|
||||
if (favorites.length === 0) {
|
||||
job.updateProgress(100);
|
||||
@@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService {
|
||||
cursor = favorites.at(-1)?.id ?? null;
|
||||
|
||||
for (const favorite of favorites) {
|
||||
const noteCreatedAt = this.idService.parse(favorite.note.id).date;
|
||||
if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let poll: MiPoll | undefined;
|
||||
if (favorite.note.hasPoll) {
|
||||
poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id });
|
||||
|
||||
@@ -313,12 +313,16 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
if (ep.meta.limit) {
|
||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||
let limitActor: string;
|
||||
let limitActor: string | null;
|
||||
if (user) {
|
||||
limitActor = user.id;
|
||||
} else {
|
||||
limitActor = getIpHash(request.ip);
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
limitActor = null;
|
||||
} else {
|
||||
limitActor = getIpHash(request.ip);
|
||||
}
|
||||
}
|
||||
|
||||
const limit = Object.assign({}, ep.meta.limit);
|
||||
@@ -330,7 +334,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
|
||||
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
|
||||
|
||||
if (factor > 0) {
|
||||
if (limitActor != null && factor > 0) {
|
||||
// Rate limit
|
||||
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
|
||||
if (rateLimit != null) {
|
||||
|
||||
@@ -89,17 +89,21 @@ export class SigninApiService {
|
||||
return { error };
|
||||
}
|
||||
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
|
||||
if (rateLimit != null) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof username !== 'string') {
|
||||
|
||||
@@ -84,19 +84,23 @@ export class SigninWithPasskeyApiService {
|
||||
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
|
||||
};
|
||||
|
||||
try {
|
||||
if (request.ip === '::1' || request.ip === '127.0.0.1') {
|
||||
console.warn('request ip is localhost, maybe caused by misconfiguration of trustProxy or reverse proxy');
|
||||
} else {
|
||||
try {
|
||||
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
|
||||
// NOTE: 1 Sign-in require 2 API calls
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
|
||||
} catch (err) {
|
||||
reply.code(429);
|
||||
return {
|
||||
error: {
|
||||
message: 'Too many failed attempts to sign in. Try again later.',
|
||||
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
|
||||
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate Passkey Auth challenge with context
|
||||
|
||||
2
packages/backend/test-federation/.config/dummy.yml
Normal file
2
packages/backend/test-federation/.config/dummy.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
url: https://example.com/
|
||||
port: 3000
|
||||
29
packages/backend/test-federation/.config/example.config.json
Normal file
29
packages/backend/test-federation/.config/example.config.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"url": "https://${HOST}/",
|
||||
"port": 3000,
|
||||
"db": {
|
||||
"host": "db.${HOST}",
|
||||
"port": 5432,
|
||||
"db": "misskey",
|
||||
"user": "postgres",
|
||||
"pass": "postgres"
|
||||
},
|
||||
"dbReplications": false,
|
||||
"trustProxy": true,
|
||||
"redis": {
|
||||
"host": "redis.test",
|
||||
"port": 6379
|
||||
},
|
||||
"id": "aidx",
|
||||
"proxyBypassHosts": [
|
||||
"api.deepl.com",
|
||||
"api-free.deepl.com",
|
||||
"www.recaptcha.net",
|
||||
"hcaptcha.com",
|
||||
"challenges.cloudflare.com"
|
||||
],
|
||||
"allowedPrivateNetworks": [
|
||||
"127.0.0.1/32",
|
||||
"172.20.0.0/16"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
url: https://${HOST}/
|
||||
port: 3000
|
||||
db:
|
||||
host: db.${HOST}
|
||||
port: 5432
|
||||
db: misskey
|
||||
user: postgres
|
||||
pass: postgres
|
||||
dbReplications: false
|
||||
redis:
|
||||
host: redis.test
|
||||
port: 6379
|
||||
id: 'aidx'
|
||||
proxyBypassHosts:
|
||||
- api.deepl.com
|
||||
- api-free.deepl.com
|
||||
- www.recaptcha.net
|
||||
- hcaptcha.com
|
||||
- challenges.cloudflare.com
|
||||
allowedPrivateNetworks:
|
||||
- 127.0.0.1/32
|
||||
- 172.20.0.0/16
|
||||
@@ -37,8 +37,8 @@ services:
|
||||
- internal_network_a
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./.config/a.test.default.yml
|
||||
target: /misskey/.config/default.yml
|
||||
source: ./.config/a.test.config.json
|
||||
target: /misskey/built/._config_.json
|
||||
read_only: true
|
||||
|
||||
db.a.test:
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./volumes/db.a
|
||||
target: /var/lib/postgresql/data
|
||||
target: /var/lib/postgresql
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ services:
|
||||
- internal_network_b
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./.config/b.test.default.yml
|
||||
target: /misskey/.config/default.yml
|
||||
source: ./.config/b.test.config.json
|
||||
target: /misskey/built/._config_.json
|
||||
read_only: true
|
||||
|
||||
db.b.test:
|
||||
@@ -50,7 +50,7 @@ services:
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ./volumes/db.b
|
||||
target: /var/lib/postgresql/data
|
||||
target: /var/lib/postgresql
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ services:
|
||||
- type: bind
|
||||
source: ../../../built
|
||||
target: /misskey/built
|
||||
read_only: false
|
||||
- type: bind
|
||||
source: ./.config/dummy.yml
|
||||
target: /misskey/.config/default.yml
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../assets
|
||||
@@ -42,6 +46,10 @@ services:
|
||||
source: ../package.json
|
||||
target: /misskey/packages/backend/package.json
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../scripts/compile_config.js
|
||||
target: /misskey/packages/backend/scripts/compile_config.js
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-js/built
|
||||
target: /misskey/packages/misskey-js/built
|
||||
|
||||
@@ -54,6 +54,10 @@ services:
|
||||
source: ../jest.js
|
||||
target: /misskey/packages/backend/jest.js
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../scripts/compile_config.js
|
||||
target: /misskey/packages/backend/scripts/compile_config.js
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-js/built
|
||||
target: /misskey/packages/misskey-js/built
|
||||
|
||||
@@ -28,7 +28,7 @@ function generate {
|
||||
-days 500
|
||||
if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
|
||||
if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
|
||||
if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
|
||||
if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.config.json > .config/$1.config.json; fi
|
||||
}
|
||||
|
||||
generate a.test
|
||||
|
||||
@@ -168,7 +168,36 @@ describe('export-clips', () => {
|
||||
assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2');
|
||||
});
|
||||
|
||||
test('Clipping other user\'s note', async () => {
|
||||
test('Clipping other user\'s note (followers only notes are excluded when not following)', async () => {
|
||||
const 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',
|
||||
});
|
||||
|
||||
const res2 = await api('clips/add-note', {
|
||||
clipId: clip.id,
|
||||
noteId: note.id,
|
||||
}, alice);
|
||||
assert.strictEqual(res2.status, 204);
|
||||
|
||||
const res3 = await api('i/export-clips', {}, alice);
|
||||
assert.strictEqual(res3.status, 204);
|
||||
|
||||
const exported = await pollFirstDriveFile();
|
||||
assert.strictEqual(exported[0].clipNotes.length, 0);
|
||||
});
|
||||
|
||||
test('Clipping other user\'s note (followers only notes are included when following)', async () => {
|
||||
// Alice follows Bob
|
||||
await api('following/create', { userId: bob.id }, alice);
|
||||
|
||||
const res = await api('clips/create', {
|
||||
name: 'kawaii',
|
||||
description: 'kawaii',
|
||||
|
||||
136
packages/backend/test/unit/misc/should-hide-note-by-time.ts
Normal file
136
packages/backend/test/unit/misc/should-hide-note-by-time.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
|
||||
|
||||
describe('misc:should-hide-note-by-time', () => {
|
||||
let clock: lolex.InstalledClock;
|
||||
const epoch = Date.UTC(2000, 0, 1, 0, 0, 0);
|
||||
|
||||
beforeEach(() => {
|
||||
clock = lolex.install({
|
||||
// https://github.com/sinonjs/sinon/issues/2620
|
||||
toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[],
|
||||
now: new Date(epoch),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
describe('hiddenBefore が null または undefined の場合', () => {
|
||||
test('hiddenBefore が null のときは false を返す(非表示機能が有効でない)', () => {
|
||||
const createdAt = new Date(epoch - 86400000); // 1 day ago
|
||||
expect(shouldHideNoteByTime(null, createdAt)).toBe(false);
|
||||
});
|
||||
|
||||
test('hiddenBefore が undefined のときは false を返す(非表示機能が有効でない)', () => {
|
||||
const createdAt = new Date(epoch - 86400000); // 1 day ago
|
||||
expect(shouldHideNoteByTime(undefined, createdAt)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('相対時間モード (hiddenBefore <= 0)', () => {
|
||||
test('閾値内に作成されたノートは false を返す(作成からの経過時間がまだ短い→表示)', () => {
|
||||
const hiddenBefore = -86400; // 1 day in seconds
|
||||
const createdAt = new Date(epoch - 3600000); // 1 hour ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
|
||||
});
|
||||
|
||||
test('閾値を超えて作成されたノートは true を返す(指定期間以上経過している→非表示)', () => {
|
||||
const hiddenBefore = -86400; // 1 day in seconds
|
||||
const createdAt = new Date(epoch - 172800000); // 2 days ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('ちょうど閾値で作成されたノートは true を返す(閾値に達したら非表示)', () => {
|
||||
const hiddenBefore = -86400; // 1 day in seconds
|
||||
const createdAt = new Date(epoch - 86400000); // exactly 1 day ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('異なる相対時間値で判定できる(1時間設定と3時間設定の異なる結果)', () => {
|
||||
const createdAt = new Date(epoch - 7200000); // 2 hours ago
|
||||
expect(shouldHideNoteByTime(-3600, createdAt)).toBe(true); // 1時間経過→非表示
|
||||
expect(shouldHideNoteByTime(-10800, createdAt)).toBe(false); // 3時間未経過→表示
|
||||
});
|
||||
|
||||
test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
|
||||
const createdAtString = new Date(epoch - 86400000).toISOString();
|
||||
const hiddenBefore = -86400; // 1 day in seconds
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAtString)).toBe(true);
|
||||
});
|
||||
|
||||
test('hiddenBefore が 0 の場合に対応できる(0秒以上経過で非表示→ほぼ全て非表示)', () => {
|
||||
const hiddenBefore = 0;
|
||||
const createdAt = new Date(epoch - 1); // 1ms ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('絶対時間モード (hiddenBefore > 0)', () => {
|
||||
test('閾値タイムスタンプより後に作成されたノートは false を返す(指定日時より後→表示)', () => {
|
||||
const thresholdSeconds = Math.floor(epoch / 1000);
|
||||
const createdAt = new Date(epoch + 3600000); // 1 hour from epoch
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(false);
|
||||
});
|
||||
|
||||
test('閾値タイムスタンプより前に作成されたノートは true を返す(指定日時より前→非表示)', () => {
|
||||
const thresholdSeconds = Math.floor(epoch / 1000);
|
||||
const createdAt = new Date(epoch - 3600000); // 1 hour ago
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('ちょうど閾値タイムスタンプで作成されたノートは true を返す(指定日時に達したら非表示)', () => {
|
||||
const thresholdSeconds = Math.floor(epoch / 1000);
|
||||
const createdAt = new Date(epoch); // exactly epoch
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
|
||||
const thresholdSeconds = Math.floor(epoch / 1000);
|
||||
const createdAtString = new Date(epoch - 3600000).toISOString();
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAtString)).toBe(true);
|
||||
});
|
||||
|
||||
test('異なる閾値タイムスタンプで判定できる(2021年設定と現在より1時間前設定の異なる結果)', () => {
|
||||
const thresholdSeconds = Math.floor((epoch - 86400000) / 1000); // 1 day ago
|
||||
const createdAtBefore = new Date(epoch - 172800000); // 2 days ago
|
||||
const createdAtAfter = new Date(epoch - 3600000); // 1 hour ago
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAtBefore)).toBe(true); // 閾値より前→非表示
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAtAfter)).toBe(false); // 閾値より後→表示
|
||||
});
|
||||
});
|
||||
|
||||
describe('エッジケース', () => {
|
||||
test('相対時間モードで非常に古いノートに対応できる(非常に古い→閾値超→非表示)', () => {
|
||||
const hiddenBefore = -1; // hide notes older than 1 second
|
||||
const createdAt = new Date(epoch - 1000000); // very old
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('相対時間モードで非常に新しいノートに対応できる(非常に新しい→閾値未満→表示)', () => {
|
||||
const hiddenBefore = -86400; // 1 day
|
||||
const createdAt = new Date(epoch - 1); // 1ms ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
|
||||
});
|
||||
|
||||
test('大きなタイムスタンプ値に対応できる(未来の日時を指定→現在のノートは全て非表示)', () => {
|
||||
const thresholdSeconds = Math.floor(epoch / 1000) + 86400; // 1 day from epoch
|
||||
const createdAt = new Date(epoch); // created epoch
|
||||
expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
|
||||
});
|
||||
|
||||
test('小さな相対時間値に対応できる(1秒設定で2秒前→非表示)', () => {
|
||||
const hiddenBefore = -1; // 1 second
|
||||
const createdAt = new Date(epoch - 2000); // 2 seconds ago
|
||||
expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user