diff --git a/CHANGELOG.md b/CHANGELOG.md index cec53e3847..9cdd81d2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,24 @@ ### Client - Enhance: チャンネル指定リノートでリノート先のチャンネルに移動できるように +- Enhance: アバターデコレーションにカテゴリを設定できるように - Fix: 一部のページ内リンクが正しく動作しない問題を修正 +- Fix: ドライブへの画像アップロード時にファイル名の変更が無視される不具合を修正 +- Fix: 連合が無効化されたサーバーで一部の設定項目が空欄で表示される問題を修正 ### Server +- Enhance: メモリ使用量を削減 +- Enhance: 起動の高速化 + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/1410) +- Enhance: バックエンドの開発モード時の安定性向上 +- Fix: ファイルシステムを用いる処理におけるパスの取り扱いを改善 - Fix: `/api-doc` にアクセスできない問題を修正 - Fix: support `alsoKnownAs` from remote actors as either array or unwrapped singleton +- Fix: ローカルに存在しないリモートアカウントに対するアカウント削除リクエストを受信した際に、そのユーザーを新規作成して削除する挙動を修正 +- Fix: Inboxでの特定のエラーによる失敗はDelayedにしない +- Fix: ID生成アルゴリズムにULIDを使用している場合にMisskeyが正しく動作しない問題を修正 +- Fix: リレー経由で届いたノートがリノートとして表示される問題を修正 +- Fix: robots.txtの内容を調整 ## 2026.3.2 diff --git a/Dockerfile b/Dockerfile index 19f9e8c9dc..d6c8d7e415 100644 --- a/Dockerfile +++ b/Dockerfile @@ -102,7 +102,6 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built -COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis COPY --chown=misskey:misskey . ./ diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 6fe7f32cc4..b91e34dc12 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "lib": ["dom", "es5"], - "target": "es5", + "lib": ["dom"], + "target": "esnext", "types": ["cypress", "node"] }, "include": ["./**/*.ts"] diff --git a/package.json b/package.json index 1bd418ee5a..0d090e871f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2026.4.0-alpha.2", + "version": "2026.4.0-alpha.5", "codename": "nasubi", "repository": { "type": "git", @@ -28,9 +28,9 @@ "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js", - "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js", - "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js", + "start": "cd packages/backend && pnpm compile-config && node ./built/entry.js", + "start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/entry.js", + "start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/entry.js", "cli": "cd packages/backend && pnpm cli", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", @@ -53,8 +53,8 @@ "cleanall": "pnpm clean-all" }, "dependencies": { - "cssnano": "7.1.3", - "esbuild": "0.27.4", + "cssnano": "7.1.4", + "esbuild": "0.28.0", "execa": "9.6.1", "ignore-walk": "8.0.0", "js-yaml": "4.1.1", @@ -66,9 +66,9 @@ "@eslint/js": "9.39.4", "@misskey-dev/eslint-plugin": "2.1.0", "@types/js-yaml": "4.0.9", - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "@typescript/native-preview": "7.0.0-dev.20260116.1", "cross-env": "10.1.0", "cypress": "15.13.0", @@ -86,7 +86,7 @@ "overrides": { "@aiscript-dev/aiscript-languageserver": "-", "chokidar": "5.0.0", - "lodash": "4.17.23" + "lodash": "4.18.1" }, "ignoredBuiltDependencies": [ "@sentry-internal/node-cpu-profiler", diff --git a/packages/backend/assets/robots.txt b/packages/backend/assets/robots.txt deleted file mode 100644 index dc17e04e3f..0000000000 --- a/packages/backend/assets/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -user-agent: * -allow: / - -# todo: sitemap diff --git a/packages/backend/build.js b/packages/backend/build.js deleted file mode 100644 index 52ca09b7a8..0000000000 --- a/packages/backend/build.js +++ /dev/null @@ -1,121 +0,0 @@ -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.`); -} diff --git a/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js new file mode 100644 index 0000000000..a3410aa88e --- /dev/null +++ b/packages/backend/migration/1766652173085-add-category-to-avatar-decorations.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AddCategoryToAvatarDecorations1766652173085 { + name = 'AddCategoryToAvatarDecorations1766652173085'; + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" ADD "category" character varying(128)'); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "avatar_decoration" DROP COLUMN "category"'); + } +}; diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index 1a8c146451..dabc0893f4 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -1,6 +1,6 @@ import { DataSource } from 'typeorm'; -import { loadConfig } from './src-js/config.js'; -import { entities } from './src-js/postgres.js'; +import { loadConfig } from './built/config.js'; +import { entities } from './built/postgres.js'; const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1'; diff --git a/packages/backend/package.json b/packages/backend/package.json index 54b06e8b4d..a011eb83ab 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -7,21 +7,22 @@ "node": "^22.15.0 || ^24.10.0" }, "scripts": { - "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", + "start": "pnpm compile-config && node ./built/entry.js", + "start:inspect": "pnpm compile-config && node --inspect ./built/entry.js", + "start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/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 ./src-js/boot/cli.js", + "cli": "pnpm compile-config && node ./built/cli.js", "check:connect": "pnpm compile-config && node ./scripts/check_connect.js", "compile-config": "node ./scripts/compile_config.js", - "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", + "build": "rolldown -c", + "build:unit": "rolldown -c --sourcemap", + "build:e2e": "swc src -d src-js -D --strip-leading-paths && 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": "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", + "dev": "pnpm compile-config && rolldown -c --watch", "typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit", "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"", "lint": "pnpm typecheck && pnpm eslint", @@ -31,11 +32,11 @@ "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": "pnpm build:unit && pnpm jest", + "test:e2e": "pnpm build:e2e && 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", + "test-and-coverage": "pnpm build:unit && pnpm jest-and-coverage", + "test-and-coverage:e2e": "pnpm build:e2e && pnpm jest-and-coverage:e2e", "check-migrations": "node scripts/check_migrations_clean.js", "generate-api-json": "pnpm compile-config && node ./scripts/generate_api_json.js" }, @@ -71,29 +72,29 @@ "utf-8-validate": "6.0.6" }, "dependencies": { - "@aws-sdk/client-s3": "3.1016.0", - "@aws-sdk/lib-storage": "3.1016.0", + "@aws-sdk/client-s3": "3.1024.0", + "@aws-sdk/lib-storage": "3.1024.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.4", "@fastify/cors": "11.2.0", "@fastify/express": "4.0.4", - "@fastify/http-proxy": "11.4.2", + "@fastify/http-proxy": "11.4.3", "@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.97", - "@nestjs/common": "11.1.17", - "@nestjs/core": "11.1.17", - "@nestjs/testing": "11.1.17", + "@nestjs/common": "11.1.18", + "@nestjs/core": "11.1.18", + "@nestjs/testing": "11.1.18", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.45.0", - "@sentry/profiling-node": "10.45.0", + "@sentry/node": "10.47.0", + "@sentry/profiling-node": "10.47.0", "@simplewebauthn/server": "13.3.0", - "@sinonjs/fake-timers": "15.1.1", - "@smithy/node-http-handler": "4.5.0", - "@swc/cli": "0.8.0", + "@sinonjs/fake-timers": "15.3.0", + "@smithy/node-http-handler": "4.5.1", + "@swc/cli": "0.8.1", "@swc/core": "1.15.21", "@twemoji/parser": "16.0.0", "accepts": "1.3.8", @@ -103,7 +104,7 @@ "bcryptjs": "3.0.3", "blurhash": "2.0.5", "body-parser": "2.2.2", - "bullmq": "5.71.0", + "bullmq": "5.73.0", "cacheable-lookup": "7.0.0", "chalk": "5.6.2", "chalk-template": "1.1.2", @@ -129,7 +130,7 @@ "json5": "2.2.3", "jsonld": "9.0.0", "juice": "11.1.1", - "meilisearch": "0.56.0", + "meilisearch": "0.57.0", "mfm-js": "0.25.0", "mime-types": "3.0.2", "misskey-js": "workspace:*", @@ -139,7 +140,7 @@ "nested-property": "4.0.0", "node-fetch": "3.3.2", "node-html-parser": "7.1.0", - "nodemailer": "8.0.3", + "nodemailer": "8.0.4", "nsfwjs": "4.3.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", @@ -152,7 +153,7 @@ "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.23.3", + "re2": "1.24.0", "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", @@ -178,14 +179,15 @@ "devDependencies": { "@jest/globals": "29.7.0", "@kitajs/ts-html-plugin": "4.1.4", - "@nestjs/platform-express": "11.1.17", - "@sentry/vue": "10.45.0", + "@nestjs/platform-express": "11.1.18", + "@rollup/plugin-esm-shim": "0.1.8", + "@sentry/vue": "10.47.0", "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", "@types/archiver": "7.0.0", "@types/body-parser": "1.19.6", - "@types/color-convert": "2.0.4", + "@types/color-convert": "3.0.1", "@types/content-disposition": "0.5.9", "@types/fluent-ffmpeg": "2.1.28", "@types/http-link-header": "1.0.7", @@ -193,7 +195,7 @@ "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", "@types/ms": "2.1.0", - "@types/node": "24.12.0", + "@types/node": "24.12.2", "@types/nodemailer": "7.0.11", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", @@ -206,26 +208,25 @@ "@types/semver": "7.7.1", "@types/simple-oauth2": "5.0.8", "@types/sinonjs__fake-timers": "15.0.1", - "@types/supertest": "6.0.3", + "@types/supertest": "7.2.0", "@types/tinycolor2": "1.4.6", "@types/tmp": "0.2.6", "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "aws-sdk-client-mock": "4.1.0", "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.3", "jest": "29.7.0", "jest-mock": "29.7.0", "js-yaml": "4.1.1", - "nodemon": "3.1.14", - "pid-port": "2.1.0", + "pid-port": "2.1.1", + "rolldown": "1.0.0-rc.15", "simple-oauth2": "5.1.0", "supertest": "7.2.2", "vite": "8.0.7" diff --git a/packages/backend/rolldown.config.ts b/packages/backend/rolldown.config.ts new file mode 100644 index 0000000000..382a1f7290 --- /dev/null +++ b/packages/backend/rolldown.config.ts @@ -0,0 +1,107 @@ +import { defineConfig } from 'rolldown'; +import type { Plugin, ExternalOption } from 'rolldown'; +import { execa, execaNode } from 'execa'; +import type { ResultPromise } from 'execa'; +import esmShim from '@rollup/plugin-esm-shim'; + +/** + * Watchモード時にバックエンドの起動・停止制御を行うプラグイン + */ +function backendDevServerPlugin(): Plugin { + let backendProcess: ResultPromise | null = null; + + async function runBuildAssets() { + await execa('pnpm', ['run', 'build-assets'], { + cwd: '../../', + stdout: process.stdout, + stderr: process.stderr, + }); + } + + async function killBackendProcess() { + if (backendProcess) { + backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す + backendProcess.kill(); + await new Promise(resolve => backendProcess!.on('exit', resolve)); + backendProcess = null; + } + } + + return { + name: 'backend-dev-server', + async closeBundle() { + await runBuildAssets(); + if (backendProcess) { + await killBackendProcess(); + } + backendProcess = execaNode('./built/entry.js', { + stdout: process.stdout, + stderr: process.stderr, + env: { + NODE_ENV: 'development', + }, + }); + }, + async watchChange() { + if (backendProcess) { + await killBackendProcess(); + await runBuildAssets(); + } + }, + }; +} + +export default defineConfig((args) => { + const isWatchMode = args.watch != null && args.watch !== 'false'; + + // 通常のビルド時にexternalとするモジュール + const externalModules: ExternalOption = [ + /^slacc-.*/, + 'class-transformer', + 'class-validator', + /^@sentry\/.*/, + /^@sentry-internal\/.*/, + '@nestjs/websockets/socket-module', + '@nestjs/microservices/microservices-module', + '@nestjs/microservices', + /^@napi-rs\/.*/, + 'mock-aws-s3', + 'aws-sdk', + 'nock', + 'sharp', + 'jsdom', + 're2', + 'ipaddr.js', + 'oauth2orize', + ]; + + return { + input: [ + './src/boot/entry.ts', + './src/boot/cli.ts', + './src/config.ts', + './src/postgres.ts', + './src/server/api/openapi/gen-spec.ts', + ], + platform: 'node', + tsconfig: true, + plugins: [ + esmShim(), + (isWatchMode ? backendDevServerPlugin() : undefined), + ], + output: { + keepNames: true, + minify: !isWatchMode, + sourcemap: isWatchMode, + dir: './built', + cleanDir: !isWatchMode, + format: 'esm', + }, + watch: { + include: ['src/**/*.{ts,js,mjs,cjs,tsx,json}'], + clearScreen: false, + }, + // ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする + external: isWatchMode ? [/node_modules/] : externalModules, + }; +}); diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index a1cb839303..9e2f214e93 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -4,8 +4,8 @@ */ import Redis from 'ioredis'; -import { loadConfig } from '../src-js/config.js'; -import { createPostgresDataSource } from '../src-js/postgres.js'; +import { loadConfig } from '../built/config.js'; +import { createPostgresDataSource } from '../built/postgres.js'; const config = loadConfig(); diff --git a/packages/backend/scripts/dev.mjs b/packages/backend/scripts/dev.mjs deleted file mode 100644 index db96eaf976..0000000000 --- a/packages/backend/scripts/dev.mjs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { execa, execaNode } from 'execa'; - -/** @type {import('execa').ExecaChildProcess | undefined} */ -let backendProcess; - -async function execBuildAssets() { - await execa('pnpm', ['run', 'build-assets'], { - cwd: '../../', - stdout: process.stdout, - stderr: process.stderr, - }) -} - -function execStart() { - // pnpm run start を呼び出したいが、windowsだとプロセスグループ単位でのkillが出来ずゾンビプロセス化するので - // 上記と同等の動きをするコマンドで子・孫プロセスを作らないようにしたい - backendProcess = execaNode('./built/boot/entry.js', [], { - stdout: process.stdout, - stderr: process.stderr, - env: { - 'NODE_ENV': 'development', - }, - }); -} - -async function killProc() { - if (backendProcess) { - backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す - backendProcess.kill(); - await new Promise(resolve => backendProcess.on('exit', resolve)); - backendProcess = undefined; - } -} - -(async () => { - execaNode( - './node_modules/nodemon/bin/nodemon.js', - [ - '-w', 'src', - '-e', 'ts,js,mjs,cjs,tsx,json,pug', - '--exec', 'pnpm', 'run', 'build', - ], - { - stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], - serialization: "json", - }) - .on('message', async (message) => { - if (message.type === 'exit') { - // かならずbuild->build-assetsの順番で呼び出したいので、 - // 少々トリッキーだがnodemonからのexitイベントを利用してbuild-assets->startを行う。 - // pnpm restartをbuildが終わる前にbuild-assetsが動いてしまうので、バラバラに呼び出す必要がある - - await killProc(); - await execBuildAssets(); - execStart(); - } - }) -})(); diff --git a/packages/backend/scripts/generate_api_json.js b/packages/backend/scripts/generate_api_json.js index 237f63a4d3..8a7e0b062d 100644 --- a/packages/backend/scripts/generate_api_json.js +++ b/packages/backend/scripts/generate_api_json.js @@ -19,10 +19,10 @@ async function main() { } /** @type {import('../src/config.js')} */ - const { loadConfig } = await import('../src-js/config.js'); + const { loadConfig } = await import('../built/config.js'); /** @type {import('../src/server/api/openapi/gen-spec.js')} */ - const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js'); + const { genOpenapiSpec } = await import('../built/gen-spec.js'); const config = loadConfig(); const spec = genOpenapiSpec(config, true); diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs index 3f30e24fb4..7c058a131d 100644 --- a/packages/backend/scripts/measure-memory.mjs +++ b/packages/backend/scripts/measure-memory.mjs @@ -55,7 +55,7 @@ async function getMemoryUsage(pid) { async function measureMemory() { // Start the Misskey backend server using fork to enable IPC - const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], { + const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], { cwd: join(__dirname, '..'), env: { ...process.env, diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 435bd8dd45..adccb4dc3e 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -6,7 +6,7 @@ import { Global, Inject, Module } from '@nestjs/common'; import * as Redis from 'ioredis'; import { DataSource } from 'typeorm'; -import { MeiliSearch } from 'meilisearch'; +import { Meilisearch } from 'meilisearch'; import { MiMeta } from '@/models/Meta.js'; import { DI } from './di-symbols.js'; import { Config, loadConfig } from './config.js'; @@ -40,10 +40,10 @@ const $meilisearch: Provider = { useFactory: (config: Config) => { if (config.fulltextSearch?.provider === 'meilisearch') { if (!config.meilisearch) { - throw new Error('MeiliSearch is enabled but no configuration is provided'); + throw new Error('Meilisearch is enabled but no configuration is provided'); } - return new MeiliSearch({ + return new Meilisearch({ host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.apiKey, }); diff --git a/packages/backend/src/boot/common.ts b/packages/backend/src/boot/common.ts index 268c07582d..25cc7c6797 100644 --- a/packages/backend/src/boot/common.ts +++ b/packages/backend/src/boot/common.ts @@ -4,16 +4,12 @@ */ import { NestFactory } from '@nestjs/core'; -import { ChartManagementService } from '@/core/chart/ChartManagementService.js'; -import { QueueProcessorService } from '@/queue/QueueProcessorService.js'; import { NestLogger } from '@/NestLogger.js'; -import { QueueProcessorModule } from '@/queue/QueueProcessorModule.js'; -import { QueueStatsService } from '@/daemons/QueueStatsService.js'; -import { ServerStatsService } from '@/daemons/ServerStatsService.js'; -import { ServerService } from '@/server/ServerService.js'; -import { MainModule } from '@/MainModule.js'; export async function server() { + const { MainModule } = await import('../MainModule.js'); + const { ServerService } = await import('../server/ServerService.js'); + const app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); @@ -22,6 +18,10 @@ export async function server() { await serverService.launch(); if (process.env.NODE_ENV !== 'test') { + const { ChartManagementService } = await import('../core/chart/ChartManagementService.js'); + const { QueueStatsService } = await import('../daemons/QueueStatsService.js'); + const { ServerStatsService } = await import('../daemons/ServerStatsService.js'); + app.get(ChartManagementService).start(); app.get(QueueStatsService).start(); app.get(ServerStatsService).start(); @@ -31,6 +31,10 @@ export async function server() { } export async function jobQueue() { + const { QueueProcessorModule } = await import('../queue/QueueProcessorModule.js'); + const { QueueProcessorService } = await import('../queue/QueueProcessorService.js'); + const { ChartManagementService } = await import('../core/chart/ChartManagementService.js'); + const jobQueue = await NestFactory.createApplicationContext(QueueProcessorModule, { logger: new NestLogger(), }); diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 3a33d198a5..6e37bf9e1c 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -13,8 +13,6 @@ import chalk from 'chalk'; import Xev from 'xev'; import Logger from '@/logger.js'; import { envOption } from '../env.js'; -import { masterMain } from './master.js'; -import { workerMain } from './worker.js'; import { readyRef } from './ready.js'; import 'reflect-metadata'; @@ -71,10 +69,12 @@ process.on('exit', code => { if (!envOption.disableClustering) { if (cluster.isPrimary) { logger.info(`Start main process... pid: ${process.pid}`); + const { masterMain } = await import('./master.js'); await masterMain(); ev.mount(); } else if (cluster.isWorker) { logger.info(`Start worker process... pid: ${process.pid}`); + const { workerMain } = await import('./worker.js'); await workerMain(); } else { throw new Error('Unknown process type'); @@ -82,6 +82,7 @@ if (!envOption.disableClustering) { } else { // 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない) logger.info(`Start main process... pid: ${process.pid}`); + const { masterMain } = await import('./master.js'); await masterMain(); ev.mount(); } diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 6a83359d38..d2b11ef9f4 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -190,6 +190,7 @@ export type Config = { userAgent: string; frontendManifestExists: boolean; frontendEmbedManifestExists: boolean; + rootDir: string; mediaProxy: string; externalMediaProxyEnabled: boolean; videoThumbnailGenerator: string | null; @@ -330,6 +331,7 @@ export function loadConfig(): Config { userAgent: `Misskey/${version} (${config.url})`, frontendManifestExists: frontendManifestExists, frontendEmbedManifestExists: frontendEmbedManifestExists, + rootDir, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7), diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 7d60995a7d..855ccc8b98 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -4,27 +4,31 @@ */ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; -import { Injectable } from '@nestjs/common'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'node:path'; +import { Injectable, Inject } from '@nestjs/common'; import { Mutex } from 'async-mutex'; import fetch from 'node-fetch'; +import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import type { Config } from '@/config.js'; import type { NSFWJS, PredictionType } from 'nsfwjs/core'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - const REQUIRED_CPU_FLAGS_X64 = ['avx2', 'fma']; let isSupportedCpu: undefined | boolean = undefined; @Injectable() export class AiService { + private readonly modelDir: string; private model: NSFWJS; private modelLoadMutex: Mutex = new Mutex(); constructor( + @Inject(DI.config) + private config: Config, ) { + const md = resolve(this.config.rootDir, 'packages/backend/nsfw-model'); + this.modelDir = md.endsWith('/') ? md : md + '/'; } @bindThis @@ -46,7 +50,7 @@ export class AiService { 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 }); + this.model = await nsfw.load(pathToFileURL(this.modelDir).toString(), { size: 299 }); } }); } diff --git a/packages/backend/src/core/InternalStorageService.ts b/packages/backend/src/core/InternalStorageService.ts index 4fb8a93e49..1f2f543962 100644 --- a/packages/backend/src/core/InternalStorageService.ts +++ b/packages/backend/src/core/InternalStorageService.ts @@ -5,29 +5,25 @@ import * as fs from 'node:fs'; import * as Path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const path = Path.resolve(_dirname, '../../../../files'); - @Injectable() export class InternalStorageService { + private readonly path: string; + constructor( @Inject(DI.config) private config: Config, ) { + this.path = Path.resolve(this.config.rootDir, 'files'); } @bindThis public resolvePath(key: string) { - return Path.resolve(path, key); + return Path.resolve(this.path, key); } @bindThis @@ -37,14 +33,14 @@ export class InternalStorageService { @bindThis public saveFromPath(key: string, srcPath: string) { - fs.mkdirSync(path, { recursive: true }); + fs.mkdirSync(this.path, { recursive: true }); fs.copyFileSync(srcPath, this.resolvePath(key)); return `${this.config.url}/files/${key}`; } @bindThis public saveFromBuffer(key: string, data: Buffer) { - fs.mkdirSync(path, { recursive: true }); + fs.mkdirSync(this.path, { recursive: true }); fs.writeFileSync(this.resolvePath(key), data); return `${this.config.url}/files/${key}`; } diff --git a/packages/backend/src/core/LoggerService.ts b/packages/backend/src/core/LoggerService.ts index f102461a50..db58d11e64 100644 --- a/packages/backend/src/core/LoggerService.ts +++ b/packages/backend/src/core/LoggerService.ts @@ -6,7 +6,7 @@ import { Injectable } from '@nestjs/common'; import Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import type { KEYWORD } from 'color-convert/conversions.js'; +import type { Keyword } from 'color-convert'; @Injectable() export class LoggerService { @@ -15,7 +15,7 @@ export class LoggerService { } @bindThis - public getLogger(domain: string, color?: KEYWORD | undefined) { + public getLogger(domain: string, color?: Keyword | undefined) { return new Logger(domain, color); } } diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 9120de1f9f..d96d6c70d0 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -91,13 +91,27 @@ export class RelayService { return JSON.stringify(result); } + @bindThis + private getAcceptedRelays(): Promise { + return this.relaysCache.fetch(() => this.relaysRepository.findBy({ + status: 'accepted', + })); + } + + @bindThis + public async isRelayActor(actor: { inbox: string | null; sharedInbox: string | null }): Promise { + const relays = await this.getAcceptedRelays(); + return relays.some(relay => + (actor.inbox != null && relay.inbox === actor.inbox) + || (actor.sharedInbox != null && relay.inbox === actor.sharedInbox), + ); + } + @bindThis public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise { if (activity == null) return; - const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ - status: 'accepted', - })); + const relays = await this.getAcceptedRelays(); if (relays.length === 0) return; const copy = deepClone(activity); diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index 87097ada93..91cc90db34 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -17,7 +17,7 @@ import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; import { IdService } from '@/core/IdService.js'; import { LoggerService } from '@/core/LoggerService.js'; -import type { Index, MeiliSearch } from 'meilisearch'; +import type { Index, Meilisearch } from 'meilisearch'; type K = string; type V = string | number | boolean; @@ -85,7 +85,7 @@ export class SearchService { private config: Config, @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, + private meilisearch: Meilisearch | null, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -187,7 +187,7 @@ export class SearchService { return this.searchNoteByLike(q, me, opts, pagination); } case 'meilisearch': { - return this.searchNoteByMeiliSearch(q, me, opts, pagination); + return this.searchNoteByMeilisearch(q, me, opts, pagination); } default: { const _: never = this.provider; @@ -239,14 +239,14 @@ export class SearchService { } @bindThis - private async searchNoteByMeiliSearch( + private async searchNoteByMeilisearch( q: string, me: MiUser | null, opts: SearchOpts, pagination: SearchPagination, ): Promise { if (!this.meilisearch || !this.meilisearchNoteIndex) { - throw new Error('MeiliSearch is not available'); + throw new Error('Meilisearch is not available'); } const filter: Q = { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index ff47ca930d..4f926b99d4 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -302,12 +302,14 @@ export class ApInboxService { @bindThis private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise { - const uri = getApId(activity); - if (actor.isSuspended) { return; } + // リレーからのAnnounceかチェック + const fromRelay = await this.relayService.isRelayActor(actor); + const uri = getApId(fromRelay ? target : activity); + // アナウンス先が許可されているかチェック if (!this.utilityService.isFederationAllowedUri(uri)) return; @@ -336,6 +338,14 @@ export class ApInboxService { throw err; } + // リレーからのAnnounceはリノートを作成せず、ノートを直接公開する + if (fromRelay) { + this.logger.info(`Publishing relay-delivered note: ${uri}`); + const noteObj = await this.noteEntityService.pack(renote, null, { skipHide: true, withReactionAndUserPairCache: true }); + this.globalEventService.publishNotesStream(noteObj); + return; + } + if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { return 'skip: invalid actor for this activity'; } diff --git a/packages/backend/src/logger.ts b/packages/backend/src/logger.ts index ff5363a425..ce76f8d05e 100644 --- a/packages/backend/src/logger.ts +++ b/packages/backend/src/logger.ts @@ -9,11 +9,11 @@ import { default as convertColor } from 'color-convert'; import { format as dateFormat } from 'date-fns'; import { bindThis } from '@/decorators.js'; import { envOption } from './env.js'; -import type { KEYWORD } from 'color-convert/conversions.js'; +import type { Keyword } from 'color-convert'; type Context = { name: string; - color?: KEYWORD; + color?: Keyword; }; type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; @@ -23,7 +23,7 @@ export default class Logger { private context: Context; private parentLogger: Logger | null = null; - constructor(context: string, color?: KEYWORD) { + constructor(context: string, color?: Keyword) { this.context = { name: context, color: color, @@ -31,7 +31,7 @@ export default class Logger { } @bindThis - public createSubLogger(context: string, color?: KEYWORD): Logger { + public createSubLogger(context: string, color?: Keyword): Logger { const logger = new Logger(context, color); logger.parentLogger = this; return logger; diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index 8b81702d19..291a33385f 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -5,12 +5,19 @@ // Crockford's Base32 // https://github.com/ulid/spec#encoding -import { parseBigInt32 } from '@/misc/bigint.js'; const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; +function parseBigIntCrockford(str: string): bigint { + let result = 0n; + for (let i = 0; i < str.length; i++) { + result = result * 32n + BigInt(CHARS.indexOf(str[i])); + } + return result; +} + function parseBase32(timestamp: string) { let time = 0; for (let i = 0; i < timestamp.length; i++) { @@ -26,6 +33,6 @@ export function parseUlid(id: string): { date: Date; } { export function parseUlidFull(id: string): { date: number; additional: bigint; } { return { date: parseBase32(id.slice(0, 10)), - additional: parseBigInt32(id.slice(10, 26)), + additional: parseBigIntCrockford(id.slice(10, 26)), }; } diff --git a/packages/backend/src/models/AvatarDecoration.ts b/packages/backend/src/models/AvatarDecoration.ts index 13f0b05667..d14a210392 100644 --- a/packages/backend/src/models/AvatarDecoration.ts +++ b/packages/backend/src/models/AvatarDecoration.ts @@ -36,4 +36,9 @@ export class MiAvatarDecoration { array: true, length: 128, default: '{}', }) public roleIdsThatCanBeUsedThisDecoration: string[]; + + @Column('varchar', { + length: 128, nullable: true, + }) + public category: string | null; } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 079e014da8..73fad43eb4 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -13,7 +13,7 @@ import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataServic import InstanceChart from '@/core/chart/charts/instance.js'; import ApRequestChart from '@/core/chart/charts/ap-request.js'; import FederationChart from '@/core/chart/charts/federation.js'; -import { getApId } from '@/core/activitypub/type.js'; +import { getApId, isActor, isDelete } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; @@ -84,6 +84,23 @@ export class InboxProcessorService implements OnApplicationShutdown { return `Old keyId is no longer supported. ${keyIdLower}`; } + { + let userExistenceCheckApId: string | null = null; + + // 存在しないActorに対するActorのDeleteアクティビティは無視する。 + // actorとobjectが同じならばそれはActorに違いない + if (isDelete(activity) && typeof activity.object === 'object' && (isActor(activity.object) || getApId(activity.actor) === getApId(activity.object))) { + userExistenceCheckApId = getApId(activity.object); + } + + if (userExistenceCheckApId != null) { + const user = await this.apDbResolverService.getUserFromApId(userExistenceCheckApId); + if (user == null) { + throw new Bull.UnrecoverableError(`skip: user not found for delete activity. ${getApId(userExistenceCheckApId)}`); + } + } + } + // HTTP-Signature keyIdを元にDBから取得 let authUser: { user: MiRemoteUser; @@ -235,6 +252,9 @@ export class InboxProcessorService implements OnApplicationShutdown { if (e.id === 'd450b8a9-48e4-4dab-ae36-f4db763fda7c') { // invalid Note return e.message; } + if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { + return 'note contains too many mentions'; + } } throw e; } diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index f5034d0733..4a5ac799ad 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -4,8 +4,7 @@ */ import * as fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; +import { resolve } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import type { Config } from '@/config.js'; import type { DriveFilesRepository } from '@/models/_.js'; @@ -25,11 +24,6 @@ import { FileServerFileResolver } from './file/FileServerFileResolver.js'; import { FileServerProxyHandler } from './file/FileServerProxyHandler.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -const assets = `${_dirname}/../../server/file/assets/`; - @Injectable() export class FileServerService { private logger: Logger; @@ -37,6 +31,8 @@ export class FileServerService { private proxyHandler: FileServerProxyHandler; private fileResolver: FileServerFileResolver; + private readonly assets: string; + constructor( @Inject(DI.config) private config: Config, @@ -52,6 +48,7 @@ export class FileServerService { private loggerService: LoggerService, ) { this.logger = this.loggerService.getLogger('server', 'gray'); + this.assets = resolve(this.config.rootDir, 'packages/backend/src/server/file/assets'); this.fileResolver = new FileServerFileResolver( this.driveFilesRepository, this.fileInfoService, @@ -61,13 +58,13 @@ export class FileServerService { this.driveHandler = new FileServerDriveHandler( this.config, this.fileResolver, - assets, + this.assets, this.videoProcessingService, ); this.proxyHandler = new FileServerProxyHandler( this.config, this.fileResolver, - assets, + this.assets, this.imageProcessingService, ); @@ -87,7 +84,7 @@ export class FileServerService { fastify.register((fastify, options, done) => { fastify.addHook('onRequest', handleRequestRedirectToOmitSearch); fastify.get('/files/app-default.jpg', (request, reply) => { - const file = fs.createReadStream(`${_dirname}/assets/dummy.png`); + const file = fs.createReadStream(`${this.assets}/dummy.png`); reply.header('Content-Type', 'image/jpeg'); reply.header('Cache-Control', 'max-age=31536000, immutable'); return reply.send(file); @@ -121,7 +118,7 @@ export class FileServerService { reply.header('Cache-Control', 'max-age=300'); if (request.query && 'fallback' in request.query) { - return reply.sendFile('/dummy.png', assets); + return reply.sendFile('/dummy.png', this.assets); } if (err instanceof StatusError && (err.statusCode === 302 || err.isClientError)) { diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts index 5980609f02..7c9710c693 100644 --- a/packages/backend/src/server/HealthServerService.ts +++ b/packages/backend/src/server/HealthServerService.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import { readyRef } from '@/boot/ready.js'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; -import type { MeiliSearch } from 'meilisearch'; +import type { Meilisearch } from 'meilisearch'; @Injectable() export class HealthServerService { @@ -34,7 +34,7 @@ export class HealthServerService { private db: DataSource, @Inject(DI.meilisearch) - private meilisearch: MeiliSearch | null, + private meilisearch: Meilisearch | null, ) {} @bindThis diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts index 0121c302ac..baa87dbbbe 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/create.ts @@ -55,6 +55,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -68,6 +72,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['name', 'description', 'url'], } as const; @@ -84,6 +89,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); return { @@ -94,6 +100,7 @@ export default class extends Endpoint { // eslint- description: created.description, url: created.url, roleIdsThatCanBeUsedThisDecoration: created.roleIdsThatCanBeUsedThisDecoration, + category: created.category, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts index 765bfd6766..7be3d79fee 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts @@ -60,6 +60,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -95,6 +99,7 @@ export default class extends Endpoint { // eslint- description: avatarDecoration.description, url: avatarDecoration.url, roleIdsThatCanBeUsedThisDecoration: avatarDecoration.roleIdsThatCanBeUsedThisDecoration, + category: avatarDecoration.category, })); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts index 22476a6888..b84b4c5085 100644 --- a/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/avatar-decorations/update.ts @@ -30,6 +30,7 @@ export const paramDef = { roleIdsThatCanBeUsedThisDecoration: { type: 'array', items: { type: 'string', } }, + category: { type: 'string', nullable: true }, }, required: ['id'], } as const; @@ -45,6 +46,7 @@ export default class extends Endpoint { // eslint- description: ps.description, url: ps.url, roleIdsThatCanBeUsedThisDecoration: ps.roleIdsThatCanBeUsedThisDecoration, + category: ps.category, }, me); }); } diff --git a/packages/backend/src/server/api/endpoints/drive/folders/update.ts b/packages/backend/src/server/api/endpoints/drive/folders/update.ts index 62b04e1df3..58610c76b4 100644 --- a/packages/backend/src/server/api/endpoints/drive/folders/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/folders/update.ts @@ -119,7 +119,7 @@ export default class extends Endpoint { // eslint- } // Update - this.driveFoldersRepository.update(folder.id, { + await this.driveFoldersRepository.update(folder.id, { name: folder.name, parentId: folder.parentId, }); diff --git a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts index 52acee1cfb..ca0a5e2e25 100644 --- a/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts +++ b/packages/backend/src/server/api/endpoints/get-avatar-decorations.ts @@ -49,6 +49,10 @@ export const meta = { format: 'id', }, }, + category: { + type: 'string', + optional: true, nullable: true, + }, }, }, }, @@ -76,6 +80,7 @@ export default class extends Endpoint { // eslint- description: decoration.description, url: decoration.url, roleIdsThatCanBeUsedThisDecoration: decoration.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(role => role.id === roleId)), + category: decoration.category, })); }); } diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 24bc619e79..7bbe78af61 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -4,9 +4,7 @@ */ import { randomUUID } from 'node:crypto'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import * as fs from 'node:fs'; +import { resolve } from 'node:path'; import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import sharp from 'sharp'; @@ -67,35 +65,17 @@ import { ErrorPage } from './views/error.js'; import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -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; -} - -const backendRootDir = resolve(rootDir, 'packages/backend'); -const frontendRootDir = resolve(rootDir, 'packages/frontend'); - -const staticAssets = resolve(backendRootDir, 'assets'); -const clientAssets = resolve(frontendRootDir, 'assets'); -const assets = resolve(rootDir, 'built/_frontend_dist_'); -const swAssets = resolve(rootDir, 'built/_sw_dist_'); -const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist'); -const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg'); -const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_'); -const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_'); -const tarball = resolve(rootDir, 'built/tarball'); - @Injectable() export class ClientServerService { - private logger: Logger; + private readonly staticAssets: string; + private readonly clientAssets: string; + private readonly assets: string; + private readonly swAssets: string; + private readonly fluentEmojisDir: string; + private readonly twemojiDir: string; + private readonly frontendViteOut: string; + private readonly frontendEmbedViteOut: string; + private readonly tarball: string; constructor( @Inject(DI.config) @@ -149,6 +129,17 @@ export class ClientServerService { private clientLoggerService: ClientLoggerService, ) { //this.createServer = this.createServer.bind(this); + const backendRootdir = resolve(this.config.rootDir, 'packages/backend'); + const frontendRootdir = resolve(this.config.rootDir, 'packages/frontend'); + this.staticAssets = resolve(backendRootdir, 'assets'); + this.clientAssets = resolve(frontendRootdir, 'assets'); + this.assets = resolve(this.config.rootDir, 'built/_frontend_dist_'); + this.swAssets = resolve(this.config.rootDir, 'built/_sw_dist_'); + this.fluentEmojisDir = resolve(this.config.rootDir, 'fluent-emojis/dist'); + this.twemojiDir = resolve(backendRootdir, 'node_modules/@discordapp/twemoji/dist/svg'); + this.frontendViteOut = resolve(this.config.rootDir, 'built/_frontend_vite_'); + this.frontendEmbedViteOut = resolve(this.config.rootDir, 'built/_frontend_embed_vite_'); + this.tarball = resolve(this.config.rootDir, 'built/tarball'); } @bindThis @@ -223,17 +214,17 @@ export class ClientServerService { //#region vite assets if (this.config.frontendEmbedManifestExists) { - console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`); + console.log(`[ClientServerService] Using built frontend vite assets. ${this.frontendViteOut}`); fastify.register((fastify, options, done) => { fastify.register(fastifyStatic, { - root: frontendViteOut, + root: this.frontendViteOut, prefix: '/vite/', maxAge: ms('30 days'), immutable: true, decorateReply: false, }); fastify.register(fastifyStatic, { - root: frontendEmbedViteOut, + root: this.frontendEmbedViteOut, prefix: '/embed_vite/', maxAge: ms('30 days'), immutable: true, @@ -265,21 +256,21 @@ export class ClientServerService { //#region static assets fastify.register(fastifyStatic, { - root: staticAssets, + root: this.staticAssets, prefix: '/static-assets/', maxAge: ms('7 days'), decorateReply: false, }); fastify.register(fastifyStatic, { - root: clientAssets, + root: this.clientAssets, prefix: '/client-assets/', maxAge: ms('7 days'), decorateReply: false, }); fastify.register(fastifyStatic, { - root: assets, + root: this.assets, prefix: '/assets/', maxAge: ms('7 days'), decorateReply: false, @@ -287,7 +278,7 @@ export class ClientServerService { fastify.register((fastify, options, done) => { fastify.register(fastifyStatic, { - root: tarball, + root: this.tarball, prefix: '/tarball/', maxAge: ms('30 days'), immutable: true, @@ -298,11 +289,11 @@ export class ClientServerService { }); fastify.get('/favicon.ico', async (request, reply) => { - return reply.sendFile('/favicon.ico', staticAssets); + return reply.sendFile('/favicon.ico', this.staticAssets); }); fastify.get('/apple-touch-icon.png', async (request, reply) => { - return reply.sendFile('/apple-touch-icon.png', staticAssets); + return reply.sendFile('/apple-touch-icon.png', this.staticAssets); }); fastify.get<{ Params: { path: string } }>('/fluent-emoji/:path(.*)', async (request, reply) => { @@ -315,7 +306,7 @@ export class ClientServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - return reply.sendFile(path, fluentEmojisDir, { + return reply.sendFile(path, this.fluentEmojisDir, { maxAge: ms('30 days'), }); }); @@ -330,7 +321,7 @@ export class ClientServerService { reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\''); - return reply.sendFile(path, twemojiDir, { + return reply.sendFile(path, this.twemojiDir, { maxAge: ms('30 days'), }); }); @@ -344,7 +335,7 @@ export class ClientServerService { } const mask = await sharp( - `${twemojiDir}/${path.replace('.png', '')}.svg`, + `${this.twemojiDir}/${path.replace('.png', '')}.svg`, { density: 1000 }, ) .resize(488, 488) @@ -380,7 +371,7 @@ export class ClientServerService { // ServiceWorker fastify.get('/sw.js', async (request, reply) => { - return await reply.sendFile('/sw.js', swAssets, { + return await reply.sendFile('/sw.js', this.swAssets, { maxAge: ms('10 minutes'), }); }); @@ -390,13 +381,40 @@ export class ClientServerService { // Embed Javascript fastify.get('/embed.js', async (request, reply) => { - return await reply.sendFile('/embed.js', staticAssets, { + return await reply.sendFile('/embed.js', this.staticAssets, { maxAge: ms('1 day'), }); }); fastify.get('/robots.txt', async (request, reply) => { - return await reply.sendFile('/robots.txt', staticAssets); + const disallowedPaths = [ + '/settings', + '/admin', + '/custom-emojis-manager', + '/avatar-decorations', + '/share', + '/my', + '/api', + '/inbox', + '/oauth', + '/proxy', + '/url', + ]; + + if (this.meta.ugcVisibilityForVisitor === 'none') { + disallowedPaths.push( + '/@', + '/notes', + ); + } + + let content = `User-agent: *\n`; + content += disallowedPaths.map((path) => `Disallow: ${path}`).join('\n') + '\n'; + content += 'Allow: /\n'; + content += '\n# todo: sitemap\n'; + + reply.header('Content-Type', 'text/plain; charset=utf-8'); + return await reply.send(content); }); // OpenSearch XML diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 36272c81d5..2859b2b985 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -3,9 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { promises as fsp, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { promises as fsp } from 'node:fs'; import { languages } from 'i18n/const'; import { Injectable, Inject } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; @@ -18,25 +17,11 @@ import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; import type { CommonData, ViteFiles } from './views/_.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); - -let rootDir = _dirname; -// 見つかるまで上に遡る -while (!existsSync(resolve(rootDir, 'packages'))) { - const parentDir = dirname(rootDir); - if (parentDir === rootDir) { - throw new Error('Cannot find root directory'); - } - rootDir = parentDir; -} - -const frontendViteBuilt = resolve(rootDir, 'built/_frontend_vite_'); -const frontendEmbedViteBuilt = resolve(rootDir, 'built/_frontend_embed_vite_'); - @Injectable() export class HtmlTemplateService { private frontendAssetsFetched = false; + private readonly frontendViteBuilt: string; + private readonly frontendEmbedViteBuilt: string; public frontendViteFiles: ViteFiles | null = null; public frontendBootloaderJs: string | null = null; public frontendBootloaderCss: string | null = null; @@ -53,6 +38,8 @@ export class HtmlTemplateService { private metaEntityService: MetaEntityService, ) { + this.frontendViteBuilt = resolve(this.config.rootDir, 'built/_frontend_vite_'); + this.frontendEmbedViteBuilt = resolve(this.config.rootDir, 'built/_frontend_embed_vite_'); } // 初期ロードで読み込むべきファイルのパスを収集する。 @@ -118,22 +105,22 @@ export class HtmlTemplateService { embedBootJs, embedBootCss, ] = await Promise.all([ - fsp.readFile(resolve(frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), - fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), ]); let feViteManifest: Manifest | null = null; let embedFeViteManifest: Manifest | null = null; if (this.config.frontendManifestExists) { - const manifestContent = await fsp.readFile(resolve(frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + const manifestContent = await fsp.readFile(resolve(this.frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); feViteManifest = manifestContent ? JSON.parse(manifestContent) : null; } if (this.config.frontendEmbedManifestExists) { - const manifestContent = await fsp.readFile(resolve(frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + const manifestContent = await fsp.readFile(resolve(this.frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null; } diff --git a/packages/backend/test-server/tsconfig.json b/packages/backend/test-server/tsconfig.json index 7ed7c10ed7..c773f779e6 100644 --- a/packages/backend/test-server/tsconfig.json +++ b/packages/backend/test-server/tsconfig.json @@ -26,7 +26,6 @@ "jsx": "react-jsx", "jsxImportSource": "@kitajs/html", "rootDir": "../src", - "baseUrl": "./", "paths": { "@/*": ["../src/*"] }, diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 72f26a38e0..2148844c40 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -172,7 +172,7 @@ describe('Streaming', () => { const fired = await waitFire( ayano, 'homeTimeline', // ayano:home () => api('notes/create', { text: 'bar', visibility: 'followers', replyId: note.id }, kyoko), // kyoko posts - msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.reply.text === 'foo', + msg => msg.type === 'note' && msg.body.userId === kyoko.id && msg.body.replyId === note.id, ); assert.strictEqual(fired, true); diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 9b17b1fbb9..b6902438f7 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -44,7 +44,7 @@ describe('RoleService', () => { let roleAssignmentsRepository: RoleAssignmentsRepository; let meta: jest.Mocked; let notificationService: jest.Mocked; - let clock: lolex.InstalledClock; + let clock: lolex.Clock; async function createUser(data: Partial = {}) { const un = secureRndstr(16); @@ -163,7 +163,7 @@ describe('RoleService', () => { /** * Delete meta and roleAssignment first to avoid deadlock due to schema dependencies * https://github.com/misskey-dev/misskey/issues/16783 - */ + */ await app.get(DI.metasRepository).createQueryBuilder().delete().execute(); await roleAssignmentsRepository.createQueryBuilder().delete().execute(); await Promise.all([ diff --git a/packages/backend/test/unit/SearchService.ts b/packages/backend/test/unit/SearchService.ts index 6e17bef1c3..e05eb49099 100644 --- a/packages/backend/test/unit/SearchService.ts +++ b/packages/backend/test/unit/SearchService.ts @@ -5,7 +5,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from '@jest/globals'; import { Test, TestingModule } from '@nestjs/testing'; -import type { Index, MeiliSearch } from 'meilisearch'; +import type { Index, Meilisearch } from 'meilisearch'; import { type Config, loadConfig } from '@/config.js'; import { GlobalModule } from '@/GlobalModule.js'; import { CoreModule } from '@/core/CoreModule.js'; @@ -416,7 +416,7 @@ describe('SearchService', () => { describe('meilisearch', () => { let ctx: TestContext; - let meilisearch: MeiliSearch; + let meilisearch: Meilisearch; let meilisearchIndex: Index; let meiliConfig: Config; @@ -438,7 +438,7 @@ describe('SearchService', () => { }; ctx = await buildContext(meiliConfig); - meilisearch = ctx.app.get(DI.meilisearch) as MeiliSearch; + meilisearch = ctx.app.get(DI.meilisearch) as Meilisearch; meilisearchIndex = meilisearch.index(`${meiliConfig.meilisearch!.index}---notes`); const settingsTask = await meilisearchIndex.updateSettings(meilisearchSettings); diff --git a/packages/backend/test/unit/chart.ts b/packages/backend/test/unit/chart.ts index 364a2c2fbd..4bffb5a372 100644 --- a/packages/backend/test/unit/chart.ts +++ b/packages/backend/test/unit/chart.ts @@ -34,7 +34,7 @@ describe('Chart', () => { let testGroupedChart: TestGroupedChart; let testUniqueChart: TestUniqueChart; let testIntersectionChart: TestIntersectionChart; - let clock: lolex.InstalledClock; + let clock: lolex.Clock; beforeEach(async () => { if (db) db.destroy(); diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts index 1c463c82c6..61ffb7fa1c 100644 --- a/packages/backend/test/unit/misc/should-hide-note-by-time.ts +++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts @@ -8,7 +8,7 @@ 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; + let clock: lolex.Clock; const epoch = Date.UTC(2000, 0, 1, 0, 0, 0); beforeEach(() => { diff --git a/packages/backend/test/unit/misc/ulid.ts b/packages/backend/test/unit/misc/ulid.ts new file mode 100644 index 0000000000..b79e3bc2b4 --- /dev/null +++ b/packages/backend/test/unit/misc/ulid.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from '@jest/globals'; +import { parseUlidFull } from '@/misc/id/ulid.js'; + +// Timestamp part "01KPS7S300" encodes 1776816000000ms (2026-04-22T00:00:00.000Z) +// Verified: 1*32^8 + 19*32^7 + 22*32^6 + 25*32^5 + 7*32^4 + 25*32^3 + 3*32^2 = 1776816000000 + +describe('misc:ulid', () => { + test('parseUlidFull - timestamp is parsed correctly', () => { + // id[10..25] = all zeros (valid Crockford Base32) + // 2026-04-22T00:00:00.000Z + const { date } = parseUlidFull('01KPS7S3000000000000000000'); + expect(date).toBe(1776816000000); + }); + + test('parseUlidFull - W/X/Y/Z at id[10] (chunk 1 head) do not throw', () => { + // id[10] = W + expect(() => parseUlidFull('01KPS7S300W000000000000000')).not.toThrow(); + // id[10] = X + expect(() => parseUlidFull('01KPS7S300X000000000000000')).not.toThrow(); + // id[10] = Y + expect(() => parseUlidFull('01KPS7S300Y000000000000000')).not.toThrow(); + // id[10] = Z + expect(() => parseUlidFull('01KPS7S300Z000000000000000')).not.toThrow(); + }); + + test('parseUlidFull - W/X/Y/Z at id[16] (chunk 2 head) do not throw', () => { + // id[16] = W + expect(() => parseUlidFull('01KPS7S300ABCDEFW000000000')).not.toThrow(); + // id[16] = X + expect(() => parseUlidFull('01KPS7S300ABCDEFX000000000')).not.toThrow(); + // id[16] = Y + expect(() => parseUlidFull('01KPS7S300ABCDEFY000000000')).not.toThrow(); + // id[16] = Z + expect(() => parseUlidFull('01KPS7S300ABCDEFZ000000000')).not.toThrow(); + }); +}); diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts index 01a36c9fef..5ed42b1c1f 100644 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -24,7 +24,7 @@ const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0)); describe('CheckModeratorsActivityProcessorService', () => { let app: TestingModule; - let clock: lolex.InstalledClock; + let clock: lolex.Clock; let service: CheckModeratorsActivityProcessorService; // -------------------------------------------------------------------------------------- diff --git a/packages/backend/test/unit/server/FileServerService.ts b/packages/backend/test/unit/server/FileServerService.ts index c88175c5c7..c4fc584ea3 100644 --- a/packages/backend/test/unit/server/FileServerService.ts +++ b/packages/backend/test/unit/server/FileServerService.ts @@ -296,7 +296,7 @@ describe('FileServerService', () => { }); expect(res.statusCode).toBe(404); - expect(res.headers['cache-control']).toBe('max-age=86400'); + expect(res.headers['cache-control']).toBe('public, max-age=0'); }); test('GET /files/:key 画像配信ヘッダを検証する', async () => { diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 1270c62173..b02d3e16ee 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,16 +11,16 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "rollup": "4.60.0" + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "rollup": "4.60.1" }, "dependencies": { "estree-walker": "3.0.3", "i18n": "workspace:*", "magic-string": "0.30.21", - "rolldown": "1.0.0-rc.13", + "rolldown": "1.0.0-rc.15", "vite": "8.0.7" } } diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 244d044a93..72dbb961a4 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -24,11 +24,11 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.60.0", - "shiki": "3.23.0", + "rollup": "4.60.1", + "shiki": "4.0.2", "tinycolor2": "1.6.0", "uuid": "13.0.0", - "vue": "3.5.30" + "vue": "3.5.32" }, "devDependencies": { "@misskey-dev/summaly": "5.2.5", @@ -36,25 +36,24 @@ "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.10", - "@types/node": "24.12.0", + "@types/node": "24.12.2", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "@vitest/coverage-v8": "4.1.2", - "@vue/runtime-core": "3.5.30", + "@vue/runtime-core": "3.5.32", "acorn": "8.16.0", "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.8.0", - "happy-dom": "20.8.8", + "happy-dom": "20.8.9", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "msw": "2.12.14", - "nodemon": "3.1.14", "prettier": "3.8.1", - "sass-embedded": "1.98.0", + "sass-embedded": "1.99.0", "start-server-and-test": "2.1.5", "tsx": "4.21.0", "vite": "8.0.7", diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index c53695735f..156c7e6a5b 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,10 +21,10 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "24.12.0", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "esbuild": "0.27.4", + "@types/node": "24.12.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "esbuild": "0.28.0", "eslint-plugin-vue": "10.8.0", "nodemon": "3.1.14", "vue-eslint-parser": "10.4.0" @@ -35,6 +35,6 @@ "dependencies": { "i18n": "workspace:*", "misskey-js": "workspace:*", - "vue": "3.5.30" + "vue": "3.5.32" } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 15e87bc5f7..c41cc2d82e 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -25,7 +25,7 @@ "@github/webauthn-json": "2.1.1", "@mcaptcha/core-glue": "0.1.0-alpha-5", "@misskey-dev/browser-image-resizer": "2024.1.0", - "@sentry/vue": "10.40.0", + "@sentry/vue": "10.47.0", "@syuilo/aiscript": "1.2.1", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", @@ -66,13 +66,13 @@ "qr-code-styling": "1.9.2", "qr-scanner": "1.4.2", "sanitize-html": "2.17.2", - "shiki": "3.23.0", + "shiki": "4.0.2", "textarea-caret": "3.1.0", "three": "0.183.2", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "v-code-diff": "1.13.1", - "vue": "3.5.30", + "vue": "3.5.32", "wanakana": "5.3.1" }, "devDependencies": { @@ -81,7 +81,7 @@ "@rollup/pluginutils": "5.3.0", "@storybook/addon-essentials": "8.6.18", "@storybook/addon-interactions": "8.6.18", - "@storybook/addon-links": "10.3.3", + "@storybook/addon-links": "10.3.4", "@storybook/addon-mdx-gfm": "8.6.18", "@storybook/addon-storysource": "8.6.18", "@storybook/blocks": "8.6.18", @@ -89,13 +89,13 @@ "@storybook/core-events": "8.6.18", "@storybook/manager-api": "8.6.18", "@storybook/preview-api": "8.6.18", - "@storybook/react": "10.3.3", - "@storybook/react-vite": "10.3.3", + "@storybook/react": "10.3.4", + "@storybook/react-vite": "10.3.4", "@storybook/test": "8.6.18", "@storybook/theming": "8.6.18", "@storybook/types": "8.6.18", - "@storybook/vue3": "10.3.3", - "@storybook/vue3-vite": "10.3.3", + "@storybook/vue3": "10.3.4", + "@storybook/vue3-vite": "10.3.4", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", @@ -103,17 +103,17 @@ "@types/insert-text-at-cursor": "0.3.2", "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.10", - "@types/node": "24.12.0", + "@types/node": "24.12.2", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.16.1", "@types/seedrandom": "3.0.8", "@types/textarea-caret": "3.0.4", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "@vitest/coverage-v8": "4.1.2", - "@vue/compiler-core": "3.5.30", + "@vue/compiler-core": "3.5.32", "acorn": "8.16.0", "astring": "1.9.0", "cross-env": "10.1.0", @@ -121,25 +121,25 @@ "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.8.0", "estree-walker": "3.0.3", - "happy-dom": "20.8.8", + "happy-dom": "20.8.9", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "minimatch": "10.2.4", + "minimatch": "10.2.5", "msw": "2.12.14", "msw-storybook-addon": "2.0.6", "nodemon": "3.1.14", "prettier": "3.8.1", "react": "19.2.4", "react-dom": "19.2.4", - "rolldown": "1.0.0-rc.13", - "sass-embedded": "1.98.0", + "rolldown": "1.0.0-rc.15", + "sass-embedded": "1.99.0", "seedrandom": "3.0.5", "start-server-and-test": "2.1.5", - "storybook": "10.3.3", + "storybook": "10.3.4", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "tsx": "4.21.0", "vite": "8.0.7", - "vite-plugin-glsl": "1.5.6", + "vite-plugin-glsl": "1.6.0", "vite-plugin-turbosnap": "1.0.3", "vitest": "4.1.2", "vitest-fetch-mock": "0.4.5", diff --git a/packages/frontend/src/components/MkUploaderItems.vue b/packages/frontend/src/components/MkUploaderItems.vue index 51f7ac2d09..a196189c8b 100644 --- a/packages/frontend/src/components/MkUploaderItems.vue +++ b/packages/frontend/src/components/MkUploaderItems.vue @@ -24,7 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ item.name }} + + {{ getUploadName(item).lastIndexOf('.') != -1 ? getUploadName(item).substring(0, getUploadName(item).lastIndexOf('.')) : getUploadName(item) }} + {{ getUploadName(item).substring(getUploadName(item).lastIndexOf('.')) }} +
{{ item.file.type }} @@ -47,6 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue new file mode 100644 index 0000000000..ae36f4e279 --- /dev/null +++ b/packages/frontend/src/pages/admin/roles.policy-editor.folder.vue @@ -0,0 +1,93 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/roles.policy-editor.vue b/packages/frontend/src/pages/admin/roles.policy-editor.vue new file mode 100644 index 0000000000..f93cb703e6 --- /dev/null +++ b/packages/frontend/src/pages/admin/roles.policy-editor.vue @@ -0,0 +1,471 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index e65a3c5ba8..94fc75657a 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -17,310 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
{{ i18n.ts._role.new }} @@ -345,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only