diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-22 23:01:31 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-22 23:01:31 +0900 |
| commit | e750c9171e5f70878bd1fdb5a63effdad77c58ed (patch) | |
| tree | 8c47673054d169bfcd65bab3e96a639fbbfbab5b | |
| parent | enhance(frontend): シンタックスハイライトのエンジンをJavaScr... (diff) | |
| download | misskey-e750c9171e5f70878bd1fdb5a63effdad77c58ed.tar.gz misskey-e750c9171e5f70878bd1fdb5a63effdad77c58ed.tar.bz2 misskey-e750c9171e5f70878bd1fdb5a63effdad77c58ed.zip | |
feat: ロールでアップロード可能なファイル種別を設定可能に (#16081)
* wip
* Update RoleService.ts
* wip
* Update RoleService.ts
* Update CHANGELOG.md
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | locales/index.d.ts | 16 | ||||
| -rw-r--r-- | locales/ja-JP.yml | 4 | ||||
| -rw-r--r-- | packages/backend/src/core/DriveService.ts | 22 | ||||
| -rw-r--r-- | packages/backend/src/core/RoleService.ts | 18 | ||||
| -rw-r--r-- | packages/backend/src/models/json-schema/role.ts | 8 | ||||
| -rw-r--r-- | packages/frontend-shared/js/const.ts | 1 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUploaderDialog.vue | 3 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/roles.editor.vue | 20 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/roles.vue | 8 | ||||
| -rw-r--r-- | packages/frontend/src/utility/drive.ts | 21 | ||||
| -rw-r--r-- | 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 @@ -4023,6 +4023,10 @@ export interface Locale extends ILocale { */ "cannotUploadBecauseExceedsFileSizeLimit": string; /** + * 許可されていないファイル種別のためアップロードできません。 + */ + "cannotUploadBecauseUnallowedFileType": string; + /** * ベータ */ "beta": 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<string>(); + 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 </MkSelect> <div>{{ i18n.tsx._uploader.maxFileSizeIsX({ x: $i.policies.maxFileSizeMb + 'MB' }) }}</div> + <div>{{ i18n.ts._uploader.allowedTypes }}: {{ $i.policies.uploadableFileTypes.join(', ') }}</div> </div> </div> @@ -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 </div> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])"> + <template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template> + <template #suffix> + <span v-if="role.policies.uploadableFileTypes.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span> + <span v-else>...</span> + <span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.uploadableFileTypes)"></i></span> + </template> + <div class="_gaps"> + <MkSwitch v-model="role.policies.uploadableFileTypes.useDefault" :readonly="readonly"> + <template #label>{{ i18n.ts._role.useBaseValue }}</template> + </MkSwitch> + <MkTextarea :modelValue="role.policies.uploadableFileTypes.value.join('\n')" :disabled="role.policies.uploadableFileTypes.useDefault" :readonly="readonly" @update:modelValue="role.policies.uploadableFileTypes.value = $event.split('\n')"> + <template #caption>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</template> + </MkTextarea> + <MkRange v-model="role.policies.uploadableFileTypes.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''"> + <template #label>{{ i18n.ts._role.priority }}</template> + </MkRange> + </div> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])"> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #suffix> diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index c7f47cef55..c70ae12851 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -146,6 +146,13 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])"> + <template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template> + <template #suffix>...</template> + <MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')"> + </MkTextarea> + </MkFolder> + <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])"> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</template> @@ -312,6 +319,7 @@ import { definePage } from '@/page.js'; import { instance, fetchInstance } from '@/instance.js'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; import { useRouter } from '@/router.js'; +import MkTextarea from '@/components/MkTextarea.vue'; const router = useRouter(); const baseRoleQ = ref(''); diff --git a/packages/frontend/src/utility/drive.ts b/packages/frontend/src/utility/drive.ts index 56d3f998cb..de473c9b11 100644 --- a/packages/frontend/src/utility/drive.ts +++ b/packages/frontend/src/utility/drive.ts @@ -39,6 +39,21 @@ export function uploadFile(file: File | Blob, options: { const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => { if ($i == null) return reject(); + const allowedMimeTypes = $i.policies.uploadableFileTypes; + const isAllowedMimeType = allowedMimeTypes.some(mimeType => { + if (mimeType === '*' || mimeType === '*/*') return true; + if (mimeType.endsWith('/*')) return file.type.startsWith(mimeType.slice(0, -1)); + return file.type === mimeType; + }); + if (!isAllowedMimeType) { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseUnallowedFileType, + }); + return reject(); + } + if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) { os.alert({ type: 'error', @@ -75,6 +90,12 @@ export function uploadFile(file: File | Blob, options: { title: i18n.ts.failedToUpload, text: i18n.ts.cannotUploadBecauseNoFreeSpace, }); + } else if (res.error?.id === '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea') { + os.alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseUnallowedFileType, + }); } else { os.alert({ type: 'error', diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index baa68ef471..c0d1242aaf 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5264,6 +5264,7 @@ export type components = { canHideAds: boolean; driveCapacityMb: number; maxFileSizeMb: number; + uploadableFileTypes: string[]; alwaysMarkNsfw: boolean; canUpdateBioMedia: boolean; pinLimit: number; |