feat: ロールでアップロード可能なファイル種別を設定可能に (#16081)
* wip * Update RoleService.ts * wip * Update RoleService.ts * Update CHANGELOG.md
This commit is contained in:
parent
aaee0a788d
commit
e750c9171e
|
@ -12,6 +12,8 @@
|
|||
- モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます
|
||||
- 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます
|
||||
- デフォルト値は「ローカルのコンテンツだけ公開」になっています
|
||||
- Feat: ロールでアップロード可能なファイル種別を設定可能になりました
|
||||
- デフォルトは**テキスト、JSON、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。
|
||||
- Enhance: UIのアイコンデータの読み込みを軽量化
|
||||
|
||||
### Client
|
||||
|
|
|
@ -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": {
|
||||
/**
|
||||
|
|
|
@ -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: "バッテリー消費が多いと感じたら"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('');
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -5264,6 +5264,7 @@ export type components = {
|
|||
canHideAds: boolean;
|
||||
driveCapacityMb: number;
|
||||
maxFileSizeMb: number;
|
||||
uploadableFileTypes: string[];
|
||||
alwaysMarkNsfw: boolean;
|
||||
canUpdateBioMedia: boolean;
|
||||
pinLimit: number;
|
||||
|
|
Loading…
Reference in New Issue