From e6e8bfa591b28de29709139d4d238205d7a7e171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 22 May 2025 22:56:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(frontend):=20tabler-icons=E3=81=AE?= =?UTF-8?q?=E3=82=B5=E3=83=96=E3=82=BB=E3=83=83=E3=83=88=E5=8C=96=20(#1534?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(frontend): tabler-iconsの使用されていないアイコンを削除するように * fix * fix * fix * fix * fix * Update Changelog * enhance: tablerのCSSを使用されているクラスのみに限定 * 使用するアイコンパッケージをそろえる * Update CONTRIBUTING.md * Update CONTRIBUTING.md * spdx * typo * fix: サブセットから除外される書き方をしている部分を修正 * fix: 同じunicodeに複数のアイコンclassが割り当てられている場合に対応 * remove debug code * Update CHANGELOG.md * fix merge error * setup renovate * fix: woff2ではなくwoffに変換していたのを修正 * update deps * update changelog --- .github/workflows/check-spdx-license-id.yml | 1 + CHANGELOG.md | 1 + CONTRIBUTING.md | 6 + Dockerfile | 1 + package.json | 1 + packages/frontend-embed/package.json | 3 +- packages/frontend-embed/src/boot.ts | 6 +- packages/frontend/package.json | 3 +- packages/frontend/src/_boot_.ts | 6 +- .../src/components/MkDrive.folder.vue | 8 +- packages/icons-subsetter/README.md | 15 ++ packages/icons-subsetter/eslint.config.js | 18 +++ packages/icons-subsetter/package.json | 30 ++++ packages/icons-subsetter/src/generator.ts | 141 ++++++++++++++++++ packages/icons-subsetter/src/subsetter.ts | 81 ++++++++++ packages/icons-subsetter/tsconfig.json | 20 +++ pnpm-lock.yaml | 120 +++++++++++++-- pnpm-workspace.yaml | 1 + renovate.json5 | 6 + scripts/build-assets.mjs | 5 - scripts/clean.js | 1 + scripts/dev.mjs | 6 + 22 files changed, 457 insertions(+), 23 deletions(-) create mode 100644 packages/icons-subsetter/README.md create mode 100644 packages/icons-subsetter/eslint.config.js create mode 100644 packages/icons-subsetter/package.json create mode 100644 packages/icons-subsetter/src/generator.ts create mode 100644 packages/icons-subsetter/src/subsetter.ts create mode 100644 packages/icons-subsetter/tsconfig.json diff --git a/.github/workflows/check-spdx-license-id.yml b/.github/workflows/check-spdx-license-id.yml index bc6be308d1..e40a4557df 100644 --- a/.github/workflows/check-spdx-license-id.yml +++ b/.github/workflows/check-spdx-license-id.yml @@ -58,6 +58,7 @@ jobs: "packages/frontend/test" "packages/frontend-embed/@types" "packages/frontend-embed/src" + "packages/icons-subsetter/src" "packages/misskey-bubble-game/src" "packages/misskey-reversi/src" "packages/sw/src" diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ce5932b1..f3b6b894ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます - デフォルト値は「ローカルのコンテンツだけ公開」になっています +- Enhance: UIのアイコンデータの読み込みを軽量化 ### Client - Feat: ドライブのUIが強化されました diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3aedfa9eb..8776f8ca24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,6 +258,12 @@ Misskey uses Vue(v3) as its front-end framework. - **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.** - Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome. +## Tabler Icons +アイコンは、Production Build時に使用されていないものが削除されるようになっています。 + +**アイコンを動的に設定する際には、 `ti-${someVal}` のような、アイコン名のみを動的に変化させる実装を行わないでください。** +必ず `ti-xxx` のような完全なクラス名を含めるようにしてください。 + ## nirax niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。 **vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。** diff --git a/Dockerfile b/Dockerfile index aafaa9dc6e..77277db8cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"] COPY --link ["packages/frontend-shared/package.json", "./packages/frontend-shared/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend-embed/package.json", "./packages/frontend-embed/"] +COPY --link ["packages/icons-subsetter/package.json", "./packages/icons-subsetter/"] COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-reversi/package.json", "./packages/misskey-reversi/"] diff --git a/package.json b/package.json index abc4bcdaa9..0f050b78fe 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "packages/frontend-shared", "packages/frontend", "packages/frontend-embed", + "packages/icons-subsetter", "packages/backend", "packages/sw", "packages/misskey-js", diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 026ecd96de..440aaf860b 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -14,13 +14,13 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.1.4", - "@tabler/icons-webfont": "3.33.0", "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.2.4", "@vue/compiler-sfc": "3.5.14", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", + "icons-subsetter": "workspace:*", "frontend-shared": "workspace:*", "json5": "2.2.3", "mfm-js": "0.24.0", @@ -39,6 +39,7 @@ }, "devDependencies": { "@misskey-dev/summaly": "5.2.1", + "@tabler/icons-webfont": "3.33.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.7", "@types/micromatch": "4.0.9", diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index c1b2b58beb..459b283e23 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -6,7 +6,11 @@ // https://vitejs.dev/config/build-options.html#build-modulepreload import 'vite/modulepreload-polyfill'; -import '@tabler/icons-webfont/dist/tabler-icons.scss'; +if (import.meta.env.DEV) { + await import('@tabler/icons-webfont/dist/tabler-icons.scss'); +} else { + await import('icons-subsetter/built/tabler-icons-frontendEmbed.css'); +} import '@/style.scss'; import { createApp, defineAsyncComponent } from 'vue'; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 2dcda56ceb..c7b32b5f2d 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -26,7 +26,6 @@ "@rollup/pluginutils": "5.1.4", "@sentry/vue": "9.22.0", "@syuilo/aiscript": "0.19.0", - "@tabler/icons-webfont": "3.33.0", "@twemoji/parser": "15.1.1", "@vitejs/plugin-vue": "5.2.4", "@vue/compiler-sfc": "3.5.14", @@ -48,6 +47,7 @@ "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "frontend-shared": "workspace:*", + "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", "insert-text-at-cursor": "0.3.0", "is-file-animated": "1.0.2", @@ -99,6 +99,7 @@ "@storybook/types": "8.6.14", "@storybook/vue3": "8.6.14", "@storybook/vue3-vite": "8.6.14", + "@tabler/icons-webfont": "3.33.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.7", diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 3241f2dc92..354fb95544 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -6,7 +6,11 @@ // https://vitejs.dev/config/build-options.html#build-modulepreload import 'vite/modulepreload-polyfill'; -import '@tabler/icons-webfont/dist/tabler-icons.scss'; +if (import.meta.env.DEV) { + await import('@tabler/icons-webfont/dist/tabler-icons.scss'); +} else { + await import('icons-subsetter/built/tabler-icons-frontend.css'); +} import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue index 83472eec3d..8ba7520f35 100644 --- a/packages/frontend/src/components/MkDrive.folder.vue +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.uploadFolder }} @@ -368,16 +368,14 @@ function onContextmenu(ev: MouseEvent) { border-color: var(--MI_THEME-accent); background: var(--MI_THEME-accent); - &::after { - content: "\ea5e"; - font-family: 'tabler-icons'; + &::before { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 12px; - line-height: 22px; + line-height: 18px; } } } diff --git a/packages/icons-subsetter/README.md b/packages/icons-subsetter/README.md new file mode 100644 index 0000000000..1249d65644 --- /dev/null +++ b/packages/icons-subsetter/README.md @@ -0,0 +1,15 @@ +## これは何 + +フロントエンドの各パッケージで使用されているtabler iconsのclassをスキャンし、使用されているiconのみを抽出するツールです。 + +なお、サブセット版に無いアイコンが呼び出された場合は本物のtabler icons フォントにフォールバックするようになっています。 + +このツールは本番ビルド時にのみ使用されます(開発モードでも最初の1回だけビルドが走りますが、これは型エラーを抑制するためにファイルを置いておく用の措置です) + +現時点では `src/generator.ts` の `filesToScan` にスキャン対象のファイルが書かれています。もしこれに当てはまらないファイルをサブセットのスキャン対象とする場合はこの部分を適宜修正してください。 + +## 使い方 + +```bash +pnpm build +``` diff --git a/packages/icons-subsetter/eslint.config.js b/packages/icons-subsetter/eslint.config.js new file mode 100644 index 0000000000..957100fd8c --- /dev/null +++ b/packages/icons-subsetter/eslint.config.js @@ -0,0 +1,18 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages/icons-subsetter/package.json b/packages/icons-subsetter/package.json new file mode 100644 index 0000000000..0a28f9e038 --- /dev/null +++ b/packages/icons-subsetter/package.json @@ -0,0 +1,30 @@ +{ + "name": "icons-subsetter", + "version": "0.0.0", + "private": true, + "description": "Subset tabler-icons webfont", + "type": "module", + "scripts": { + "build": "tsx src/generator.ts", + "eslint": "eslint src/**/*.ts", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@types/node": "22.15.21", + "@types/wawoff2": "1.0.2", + "@typescript-eslint/eslint-plugin": "8.32.1", + "@typescript-eslint/parser": "8.32.1" + }, + "dependencies": { + "@tabler/icons-webfont": "3.33.0", + "harfbuzzjs": "0.4.7", + "tiny-glob": "0.2.9", + "tsx": "4.19.4", + "typescript": "5.8.3", + "wawoff2": "2.0.1" + }, + "files": [ + "built" + ] +} diff --git a/packages/icons-subsetter/src/generator.ts b/packages/icons-subsetter/src/generator.ts new file mode 100644 index 0000000000..1a9e3d8fd2 --- /dev/null +++ b/packages/icons-subsetter/src/generator.ts @@ -0,0 +1,141 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fsp, existsSync } from 'fs'; +import path from 'path'; +import glob from 'tiny-glob'; +import { generateSubsettedFont } from './subsetter.js'; + +const filesToScan = { + frontend: 'packages/frontend/src/**/*.{ts,vue}', + //frontendShared: 'packages/frontend-shared/js/**/*.{ts}', // 現時点では該当がないのでスキップ。ここをコメントアウトするときは、各フロントエンドにこのチャンクのCSSのimportを追加すること + frontendEmbed: 'packages/frontend-embed/src/**/*.{ts,vue}', +}; + +async function main() { + const start = performance.now(); + + // 1. ビルドディレクトリを削除 + if (existsSync('./built')) { + await fsp.rm('./built', { recursive: true }); + } + await fsp.mkdir('./built'); + + // 2. tabler-icons.min.cssから、class名とUnicodeのマッピングを抽出 + const css = await fsp.readFile('node_modules/@tabler/icons-webfont/dist/tabler-icons.min.css', 'utf-8'); + const cssRegex = /\.(ti-[a-z0-9-]+)::?before\s*{\n?\s*content:\s*["']\\([a-fA-F0-9]+)["'];?\n?\s*}/g; + const rgMap = new Map(); + let matches: RegExpExecArray | null; + while ((matches = cssRegex.exec(css)) !== null) { + rgMap.set(matches[1], matches[2]); + } + + // 3. tabler-icons-classes.cssから、.tiのルールを抽出 + const classTiBaseRule = css.match(/\.ti\s*{[^}]*}/)![0]; + + // 4. フォールバック用のtabler-icons.woff2をコピー + const fontPath = 'node_modules/@tabler/icons-webfont/dist/fonts/'; + await fsp.copyFile(fontPath + 'tabler-icons.woff2', './built/tabler-icons.woff2'); + + // 5. 各チャンクごとにファイルをスキャンして、使用されているアイコンを抽出 + const unicodeRangeValues = new Map(); + for (const [key, dir] of Object.entries(filesToScan)) { + console.log(`Scanning ${key}...`); + + const iconsToPack = new Set(); + + const cwd = path.resolve(process.cwd(), '../../'); + const files = await glob(dir, { cwd }); + for (const file of files) { + //console.log(`Scanning ${file}`); + const content = await fsp.readFile(path.resolve(cwd, file), 'utf-8'); + const classRegex = /ti-[a-z0-9-]+/g; + let matches: RegExpExecArray | null; + while ((matches = classRegex.exec(content)) !== null) { + const icon = matches[0]; + if (rgMap.has(icon)) { + iconsToPack.add(icon); + } + } + } + + // 6. チャンク内で使用されているアイコンのUnicodeの配列を生成 + const unicodeValues = Array.from(iconsToPack).map((icon) => parseInt(rgMap.get(icon)!, 16)); + unicodeRangeValues.set(key, unicodeValues); + } + + // 7. Tabler Iconフォントをサブセット化 + const subsettedFonts = await generateSubsettedFont(fontPath + 'tabler-icons.ttf', unicodeRangeValues); + + // 8. サブセット化したフォント・CSSを書き出し + await Promise.allSettled(Array.from(subsettedFonts.entries()).map(async ([key, buffer]) => { + const cssRules = [`@font-face { + font-family: "tabler-icons"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("./tabler-icons.woff2") format("woff2"); +}`]; + + // サブセット化したフォントの中身がある(=unicodeRangeValuesの配列が空ではない)場合のみ、サブセットしたものに関する情報を追記 + if (unicodeRangeValues.get(key)!.length > 0) { + await fsp.writeFile(`./built/tabler-icons-${key}.woff2`, buffer); + + const unicodeRangeString = (() => { + const values = unicodeRangeValues.get(key)!.sort((a, b) => a - b); + const ranges = []; + + for (let i = 0; i < values.length; i++) { + const start = values[i]; + let end = values[i]; + while (values[i + 1] === end + 1) { + end = values[i + 1]; + i++; + } + if (start === end) { + ranges.push(`U+${start.toString(16)}`); + } else if (start + 1 === end) { + ranges.push(`U+${start.toString(16)}`, `U+${end.toString(16)}`); + } else { + ranges.push(`U+${start.toString(16)}-${end.toString(16)}`); + } + } + + return ranges.join(', '); + })(); + + cssRules.push(`@font-face { + font-family: "tabler-icons"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("./tabler-icons-${key}.woff2") format("woff2"); + unicode-range: ${unicodeRangeString}; +}`); + + cssRules.push(classTiBaseRule); + + // 使用されているアイコンのclassとの対応を追記 + for (const icon of unicodeRangeValues.get(key)!) { + const iconClasses = Array.from(rgMap.entries()).filter(([_, unicode]) => parseInt(unicode, 16) === icon); + if (iconClasses.length > 1) { + console.warn(`[WARN] Multiple classes for the same unicode: ${iconClasses.map(([cls]) => cls).join(', ')}. Maybe it's deprecated?`); + } + const iconSelector = iconClasses.map(([className]) => `.${className}::before`).join(', '); + cssRules.push(`${iconSelector} { content: "\\${icon.toString(16)}"; }`); + } + } + + await fsp.writeFile(`./built/tabler-icons-${key}.css`, cssRules.join('\n') + '\n'); + })); + + const end = performance.now(); + console.log(`Done in ${Math.round((end - start) * 100) / 100}ms`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/icons-subsetter/src/subsetter.ts b/packages/icons-subsetter/src/subsetter.ts new file mode 100644 index 0000000000..cd1aed2890 --- /dev/null +++ b/packages/icons-subsetter/src/subsetter.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fsp } from 'fs'; +import { compress } from 'wawoff2'; + +export async function generateSubsettedFont(ttfPath: string, unicodeRangeValues: Map) { + const ttf = await fsp.readFile(ttfPath); + + const { + instance: { exports: harfbuzzWasm }, + }: any = await WebAssembly.instantiate(await fsp.readFile('./node_modules/harfbuzzjs/hb-subset.wasm')); + + const heapu8 = new Uint8Array(harfbuzzWasm.memory.buffer); + + const subsetFonts = new Map(); + + let i = 0; + for (const [key, unicodeValues] of unicodeRangeValues) { + i++; + console.log(`Generating subset ${i} of ${unicodeRangeValues.size}...`); + + // サブセット入力を作成 + const input = harfbuzzWasm.hb_subset_input_create_or_fail(); + if (input === 0) { + throw new Error('hb_subset_input_create_or_fail (harfbuzz) returned zero'); + } + + // フォントバッファにフォントデータをセット + const fontBuffer = harfbuzzWasm.malloc(ttf.byteLength); + heapu8.set(new Uint8Array(ttf), fontBuffer); + + // フォントフェイスを作成 + const blob = harfbuzzWasm.hb_blob_create(fontBuffer, ttf.byteLength, 2, 0, 0); + const face = harfbuzzWasm.hb_face_create(blob, 0); + harfbuzzWasm.hb_blob_destroy(blob); + + // Unicodeセットに指定されたUnicodeポイントを追加 + const inputUnicodes = harfbuzzWasm.hb_subset_input_unicode_set(input); + for (const unicode of unicodeValues) { + harfbuzzWasm.hb_set_add(inputUnicodes, unicode); + } + + // サブセットを作成 + let subset; + try { + subset = harfbuzzWasm.hb_subset_or_fail(face, input); + if (subset === 0) { + harfbuzzWasm.hb_face_destroy(face); + harfbuzzWasm.free(fontBuffer); + throw new Error('hb_subset_or_fail (harfbuzz) returned zero'); + } + } finally { + harfbuzzWasm.hb_subset_input_destroy(input); + } + + // サブセットフォントデータを取得 + const result = harfbuzzWasm.hb_face_reference_blob(subset); + const offset = harfbuzzWasm.hb_blob_get_data(result, 0); + const subsetByteLength = harfbuzzWasm.hb_blob_get_length(result); + if (subsetByteLength === 0) { + harfbuzzWasm.hb_face_destroy(face); + harfbuzzWasm.hb_blob_destroy(result); + harfbuzzWasm.free(fontBuffer); + throw new Error('hb_blob_get_length (harfbuzz) returned zero'); + } + + // サブセットフォントをバッファに格納 + subsetFonts.set(key, Buffer.from(await compress(heapu8.slice(offset, offset + subsetByteLength)))); + + // メモリを解放 + harfbuzzWasm.hb_blob_destroy(result); + harfbuzzWasm.hb_face_destroy(subset); + harfbuzzWasm.hb_face_destroy(face); + harfbuzzWasm.free(fontBuffer); + } + + return subsetFonts; +} diff --git a/packages/icons-subsetter/tsconfig.json b/packages/icons-subsetter/tsconfig.json new file mode 100644 index 0000000000..08315a91cf --- /dev/null +++ b/packages/icons-subsetter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "esModuleInterop": true, + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a8f2f4dc1..9cb9caaa94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -728,9 +728,6 @@ importers: '@syuilo/aiscript': specifier: 0.19.0 version: 0.19.0 - '@tabler/icons-webfont': - specifier: 3.33.0 - version: 3.33.0 '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 @@ -794,6 +791,9 @@ importers: frontend-shared: specifier: workspace:* version: link:../frontend-shared + icons-subsetter: + specifier: workspace:* + version: link:../icons-subsetter idb-keyval: specifier: 6.2.2 version: 6.2.2 @@ -942,6 +942,9 @@ importers: '@storybook/vue3-vite': specifier: 8.6.14 version: 8.6.14(storybook@8.6.14(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.5(@types/node@22.15.21)(sass@1.89.0)(terser@5.39.2)(tsx@4.19.4))(vue@3.5.14(typescript@5.8.3)) + '@tabler/icons-webfont': + specifier: 3.33.0 + version: 3.33.0 '@testing-library/vue': specifier: 8.1.0 version: 8.1.0(@vue/compiler-sfc@3.5.14)(@vue/server-renderer@3.5.14(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3)) @@ -1086,9 +1089,6 @@ importers: '@rollup/pluginutils': specifier: 5.1.4 version: 5.1.4(rollup@4.41.0) - '@tabler/icons-webfont': - specifier: 3.33.0 - version: 3.33.0 '@twemoji/parser': specifier: 15.1.1 version: 15.1.1 @@ -1110,6 +1110,9 @@ importers: frontend-shared: specifier: workspace:* version: link:../frontend-shared + icons-subsetter: + specifier: workspace:* + version: link:../icons-subsetter json5: specifier: 2.2.3 version: 2.2.3 @@ -1156,6 +1159,9 @@ importers: '@misskey-dev/summaly': specifier: 5.2.1 version: 5.2.1 + '@tabler/icons-webfont': + specifier: 3.33.0 + version: 3.33.0 '@testing-library/vue': specifier: 8.1.0 version: 8.1.0(@vue/compiler-sfc@3.5.14)(@vue/server-renderer@3.5.14(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3)) @@ -1272,6 +1278,40 @@ importers: specifier: 10.1.3 version: 10.1.3(eslint@9.27.0) + packages/icons-subsetter: + dependencies: + '@tabler/icons-webfont': + specifier: 3.33.0 + version: 3.33.0 + harfbuzzjs: + specifier: 0.4.7 + version: 0.4.7 + tiny-glob: + specifier: 0.2.9 + version: 0.2.9 + tsx: + specifier: 4.19.4 + version: 4.19.4 + typescript: + specifier: 5.8.3 + version: 5.8.3 + wawoff2: + specifier: 2.0.1 + version: 2.0.1 + devDependencies: + '@types/node': + specifier: 22.15.21 + version: 22.15.21 + '@types/wawoff2': + specifier: 1.0.2 + version: 1.0.2 + '@typescript-eslint/eslint-plugin': + specifier: 8.32.1 + version: 8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0)(typescript@5.8.3))(eslint@9.27.0)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: 8.32.1 + version: 8.32.1(eslint@9.27.0)(typescript@5.8.3) + packages/misskey-bubble-game: dependencies: eventemitter3: @@ -4487,6 +4527,9 @@ packages: '@types/vary@1.1.3': resolution: {integrity: sha512-XJT8/ZQCL7NUut9QDLf6l24JfAEl7bnNdgxfj50cHIpEPRJLHHDDFOAq6i+GsEmeFfH7NamhBE4c4Thtb2egWg==} + '@types/wawoff2@1.0.2': + resolution: {integrity: sha512-UVq4NxMuBywz5gklafg1HcENrK02trh/q+ifyvO6xktflGXRtf11GYsia3/l5nxg2edFQRlnUT6ivnk898WDiw==} + '@types/web-push@3.6.4': resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} @@ -6602,6 +6645,9 @@ packages: get-tsconfig@4.10.0: resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + getos@3.2.1: resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} @@ -6657,10 +6703,16 @@ packages: resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} engines: {node: '>= 0.4'} + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-protobuf@3.21.2: resolution: {integrity: sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==} @@ -6702,6 +6754,9 @@ packages: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} + harfbuzzjs@0.4.7: + resolution: {integrity: sha512-fGrMB7gk+x1ye++cN+OgDnHLLz8wg6aW26VPb8Q14V6XeZKGC0BCALe+ZEnwASU/b+YprBnRTELMJAlwy9jrLw==} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -7896,6 +7951,10 @@ packages: resolution: {integrity: sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==} engines: {node: '>=10'} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimatch@9.0.1: resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} engines: {node: '>=16 || 14 >=14.17'} @@ -8083,6 +8142,10 @@ packages: resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==} engines: {node: '>=10'} + node-abi@3.75.0: + resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==} + engines: {node: '>=10'} + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -9902,6 +9965,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -10526,6 +10592,10 @@ packages: resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==} engines: {node: '>=12'} + wawoff2@2.0.1: + resolution: {integrity: sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==} + hasBin: true + web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} engines: {node: '>= 16'} @@ -14556,6 +14626,10 @@ snapshots: dependencies: '@types/node': 22.15.21 + '@types/wawoff2@1.0.2': + dependencies: + '@types/node': 22.15.21 + '@types/web-push@3.6.4': dependencies: '@types/node': 22.15.21 @@ -17189,6 +17263,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + getos@3.2.1: dependencies: async: 3.2.4 @@ -17240,7 +17318,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 5.1.2 + minimatch: 5.1.6 once: 1.4.0 global-dirs@3.0.1: @@ -17257,6 +17335,8 @@ snapshots: dependencies: define-properties: 1.2.1 + globalyzer@0.1.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -17266,6 +17346,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + google-protobuf@3.21.2: optional: true @@ -17319,6 +17401,8 @@ snapshots: hard-rejection@2.1.0: {} + harfbuzzjs@0.4.7: {} + has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -18857,6 +18941,10 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + minimatch@9.0.1: dependencies: brace-expansion: 2.0.1 @@ -19058,6 +19146,11 @@ snapshots: dependencies: semver: 7.7.2 + node-abi@3.75.0: + dependencies: + semver: 7.7.2 + optional: true + node-abort-controller@3.1.1: {} node-addon-api@3.2.1: @@ -19746,7 +19839,7 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.74.0 + node-abi: 3.75.0 pump: 3.0.2 rc: 1.2.8 simple-get: 4.0.1 @@ -21044,6 +21137,11 @@ snapshots: through@2.3.8: {} + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -21171,7 +21269,7 @@ snapshots: tsx@4.19.4: dependencies: esbuild: 0.25.4 - get-tsconfig: 4.10.0 + get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -21638,6 +21736,10 @@ snapshots: wanakana@5.3.1: {} + wawoff2@2.0.1: + dependencies: + argparse: 2.0.1 + web-push@3.6.7: dependencies: asn1.js: 5.4.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8d4e0f73eb..9a034e257b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - packages/frontend-shared - packages/frontend - packages/frontend-embed + - packages/icons-subsetter - packages/sw - packages/misskey-js - packages/misskey-js/generator diff --git a/renovate.json5 b/renovate.json5 index 23a26eb712..a57df8befb 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -62,6 +62,12 @@ 'scripts/**/package.json', ], }, + { + groupName: '[icons-subsetter] Update dependencies', + matchFileNames: [ + 'packages/icons-subsetter/**/package.json', + ], + }, { groupName: '[GitHub Actions] Update dependencies', matchFileNames: [ diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs index 8ab341795c..e610a72380 100644 --- a/scripts/build-assets.mjs +++ b/scripts/build-assets.mjs @@ -33,10 +33,6 @@ async function copyFrontendFonts() { await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true }); } -async function copyFrontendTablerIcons() { - await fs.cp('./packages/frontend/node_modules/@tabler/icons-webfont/dist', './built/_frontend_dist_/tabler-icons', { dereference: true, recursive: true }); -} - async function copyFrontendLocales() { generateDTS(); @@ -89,7 +85,6 @@ async function buildBackendStyle() { async function build() { await Promise.all([ copyFrontendFonts(), - copyFrontendTablerIcons(), copyFrontendLocales(), copyBackendViews(), buildBackendScript(), diff --git a/scripts/clean.js b/scripts/clean.js index 86c19281ea..69a8df76af 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -10,6 +10,7 @@ const fs = require('fs'); fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true }); + fs.rmSync(__dirname + '/../packages/icons-subsetter/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/sw/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/misskey-js/built', { recursive: true, force: true }); fs.rmSync(__dirname + '/../packages/misskey-reversi/built', { recursive: true, force: true }); diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 3f66028bee..e500510b9e 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -32,6 +32,12 @@ await Promise.all([ stdout: process.stdout, stderr: process.stderr, }), + // icons-subsetterは開発段階では使用されないが、型エラーを抑制するためにはじめの一度だけビルドする + execa('pnpm', ['--filter', 'icons-subsetter', 'build'], { + cwd: _dirname + '/../', + stdout: process.stdout, + stderr: process.stderr, + }), ]); execa('pnpm', ['build-pre', '--watch'], { From aaee0a788da35dddb5012d150c815e34210ba1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 22 May 2025 22:57:04 +0900 Subject: [PATCH 2/5] =?UTF-8?q?enhance(frontend):=20=E3=82=B7=E3=83=B3?= =?UTF-8?q?=E3=82=BF=E3=83=83=E3=82=AF=E3=82=B9=E3=83=8F=E3=82=A4=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=83=88=E3=81=AE=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3?= =?UTF-8?q?=E3=82=92JavaScript=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AE?= =?UTF-8?q?=E3=82=82=E3=81=AE=E3=81=AB=E5=A4=89=E6=9B=B4=20(#16084)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(frontend): シンタックスハイライトのエンジンをJavaScriptベースのものに変更 * Update Changelog --- CHANGELOG.md | 3 +++ packages/frontend/src/utility/code-highlighter.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b6b894ca..193bc8ee76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ (Based on https://github.com/taiyme/misskey/pull/198, https://github.com/taiyme/misskey/pull/211, https://github.com/taiyme/misskey/pull/283) - Enhance: ユーザー設定でURLプレビューを無効化できるように - Enhance: AiScriptからtoastを表示する関数 `Mk:toast` を追加 +- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更 + - フロントエンドの読み込みサイズを軽量化しました + - ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。 - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 ### Server diff --git a/packages/frontend/src/utility/code-highlighter.ts b/packages/frontend/src/utility/code-highlighter.ts index 4f2aff9d4c..7dca18d58f 100644 --- a/packages/frontend/src/utility/code-highlighter.ts +++ b/packages/frontend/src/utility/code-highlighter.ts @@ -4,7 +4,7 @@ */ import { createHighlighterCore } from 'shiki/core'; -import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; +import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; @@ -71,7 +71,7 @@ async function initHighlighter() { const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); const highlighter = await createHighlighterCore({ - engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')), + engine: createJavaScriptRegexEngine({ forgiving: true }), themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), From e750c9171e5f70878bd1fdb5a63effdad77c58ed Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 22 May 2025 23:01:31 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E3=83=AD=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=81=A7=E3=82=A2=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E3=81=AA=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E7=A8=AE=E5=88=A5=E3=82=92=E8=A8=AD=E5=AE=9A=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=81=AB=20(#16081)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update RoleService.ts * wip * Update RoleService.ts * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ locales/index.d.ts | 16 ++++++++++++++ locales/ja-JP.yml | 4 ++++ packages/backend/src/core/DriveService.ts | 22 ++++++++++++++----- packages/backend/src/core/RoleService.ts | 18 +++++++++++++++ .../backend/src/models/json-schema/role.ts | 8 +++++++ packages/frontend-shared/js/const.ts | 1 + .../src/components/MkUploaderDialog.vue | 3 ++- .../frontend/src/pages/admin/roles.editor.vue | 20 +++++++++++++++++ packages/frontend/src/pages/admin/roles.vue | 8 +++++++ packages/frontend/src/utility/drive.ts | 21 ++++++++++++++++++ packages/misskey-js/src/autogen/types.ts | 1 + 12 files changed, 118 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 193bc8ee76..5ee51b3d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます - デフォルト値は「ローカルのコンテンツだけ公開」になっています +- Feat: ロールでアップロード可能なファイル種別を設定可能になりました + - デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。 - Enhance: UIのアイコンデータの読み込みを軽量化 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index 0a0e28e02e..b5a4267098 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4022,6 +4022,10 @@ export interface Locale extends ILocale { * ファイルサイズの制限を超えているためアップロードできません。 */ "cannotUploadBecauseExceedsFileSizeLimit": string; + /** + * 許可されていないファイル種別のためアップロードできません。 + */ + "cannotUploadBecauseUnallowedFileType": string; /** * ベータ */ @@ -7729,6 +7733,14 @@ export interface Locale extends ILocale { * チャットを許可 */ "chatAvailability": string; + /** + * アップロード可能なファイル種別 + */ + "uploadableFileTypes": string; + /** + * MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*) + */ + "uploadableFileTypes_caption": string; }; "_condition": { /** @@ -11925,6 +11937,10 @@ export interface Locale extends ILocale { * アップロード可能な最大ファイルサイズは{x}です。 */ "maxFileSizeIsX": ParameterizedString<"x">; + /** + * アップロード可能なファイル種別 + */ + "allowedTypes": string; }; "_clientPerformanceIssueTip": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b9e778741c..fd794fffd6 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1001,6 +1001,7 @@ failedToUpload: "アップロード失敗" cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" +cannotUploadBecauseUnallowedFileType: "許可されていないファイル種別のためアップロードできません。" beta: "ベータ" enableAutoSensitive: "自動センシティブ判定" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" @@ -2001,6 +2002,8 @@ _role: canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" chatAvailability: "チャットを許可" + uploadableFileTypes: "アップロード可能なファイル種別" + uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -3190,6 +3193,7 @@ _uploader: abortConfirm: "アップロードされていないファイルがありますが、中止しますか?" doneConfirm: "アップロードされていないファイルがありますが、完了しますか?" maxFileSizeIsX: "アップロード可能な最大ファイルサイズは{x}です。" + allowedTypes: "アップロード可能なファイル種別" _clientPerformanceIssueTip: title: "バッテリー消費が多いと感じたら" diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 0d5ac022aa..0c7c06d92f 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -515,16 +515,23 @@ export class DriveService { this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); - //#region Check drive usage + //#region Check drive usage and mime type if (user && !isLink) { - const usage = await this.driveFileEntityService.calcDriveUsageOf(user); const isLocalUser = this.userEntityService.isLocalUser(user); - const policies = await this.roleService.getUserPolicies(user.id); + + const allowedMimeTypes = policies.uploadableFileTypes; + const isAllowed = allowedMimeTypes.some((mimeType) => { + if (mimeType === '*' || mimeType === '*/*') return true; + if (mimeType.endsWith('/*')) return info.type.mime.startsWith(mimeType.slice(0, -1)); + return info.type.mime === mimeType; + }); + if (!isAllowed) { + throw new IdentifiableError('bd71c601-f9b0-4808-9137-a330647ced9b', 'Unallowed file type.'); + } + const driveCapacity = 1024 * 1024 * policies.driveCapacityMb; const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; - this.registerLogger.debug('drive capacity override applied'); - this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); if (maxFileSize < info.size) { if (isLocalUser) { @@ -532,6 +539,11 @@ export class DriveService { } } + const usage = await this.driveFileEntityService.calcDriveUsageOf(user); + + this.registerLogger.debug('drive capacity override applied'); + this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + // If usage limit exceeded if (driveCapacity < usage + info.size) { if (isLocalUser) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index fc97780ba3..2669104f7e 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -65,6 +65,7 @@ export type RolePolicies = { canImportMuting: boolean; canImportUserLists: boolean; chatAvailability: 'available' | 'readonly' | 'unavailable'; + uploadableFileTypes: string[]; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -101,6 +102,13 @@ export const DEFAULT_POLICIES: RolePolicies = { canImportMuting: true, canImportUserLists: true, chatAvailability: 'available', + uploadableFileTypes: [ + 'text/plain', + 'application/json', + 'image/*', + 'video/*', + 'audio/*', + ], }; @Injectable() @@ -412,6 +420,16 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), chatAvailability: calc('chatAvailability', aggregateChatAvailability), + uploadableFileTypes: calc('uploadableFileTypes', vs => { + const set = new Set(); + for (const v of vs) { + for (const type of v) { + if (type.trim() === '') continue; + set.add(type.trim()); + } + } + return [...set]; + }), }; } diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index e67704e8d3..8bd01c92a3 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -228,6 +228,14 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + uploadableFileTypes: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, alwaysMarkNsfw: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 8c49b41f4d..c4c4a25d74 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -110,6 +110,7 @@ export const ROLE_POLICIES = [ 'canImportMuting', 'canImportUserLists', 'chatAvailability', + 'uploadableFileTypes', ] as const; export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index b171546854..fb27dcbf58 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -69,6 +69,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}
+
{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}
@@ -281,7 +282,7 @@ function showMenu(ev: MouseEvent, item: typeof items.value[0]) { if (item.abort != null) { item.abort(); } - } + }, }); } diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 2473d4e90d..5da969b835 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -406,6 +406,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+