This commit is contained in:
syuilo 2025-05-22 16:16:06 +09:00
parent 23542530e1
commit 4bcdc6639d
11 changed files with 109 additions and 5 deletions

View File

@ -12,6 +12,8 @@
- モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます
- 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます
- デフォルト値は「ローカルのコンテンツだけ公開」になっています - デフォルト値は「ローカルのコンテンツだけ公開」になっています
- Feat: ロールでアップロード可能なファイル種別を設定可能になりました
- デフォルトは**テキスト、画像、動画、音声ファイル**になっています。zipなど、その他の種別のファイルは含まれていないため、必要に応じて設定を変更してください。
### Client ### Client
- Feat: ドライブのUIが強化されました - Feat: ドライブのUIが強化されました

12
locales/index.d.ts vendored
View File

@ -4022,6 +4022,10 @@ export interface Locale extends ILocale {
* *
*/ */
"cannotUploadBecauseExceedsFileSizeLimit": string; "cannotUploadBecauseExceedsFileSizeLimit": string;
/**
*
*/
"cannotUploadBecauseUnallowedFileType": string;
/** /**
* *
*/ */
@ -7729,6 +7733,14 @@ export interface Locale extends ILocale {
* *
*/ */
"chatAvailability": string; "chatAvailability": string;
/**
*
*/
"uploadableFileTypes": string;
/**
* MIMEタイプを指定します(*)(: image/*)
*/
"uploadableFileTypes_caption": string;
}; };
"_condition": { "_condition": {
/** /**

View File

@ -1001,6 +1001,7 @@ failedToUpload: "アップロード失敗"
cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。" cannotUploadBecauseInappropriate: "不適切な内容を含む可能性があると判定されたためアップロードできません。"
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。" cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いためアップロードできません。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルサイズの制限を超えているためアップロードできません。"
cannotUploadBecauseUnallowedFileType: "許可されていないファイル種別のためアップロードできません。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動センシティブ判定" enableAutoSensitive: "自動センシティブ判定"
enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。" enableAutoSensitiveDescription: "利用可能な場合は、機械学習を利用して自動でメディアにセンシティブフラグを設定します。この機能をオフにしても、サーバーによっては自動で設定されることがあります。"
@ -2001,6 +2002,8 @@ _role:
canImportMuting: "ミュートのインポートを許可" canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可" canImportUserLists: "リストのインポートを許可"
chatAvailability: "チャットを許可" chatAvailability: "チャットを許可"
uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"

View File

@ -515,16 +515,23 @@ export class DriveService {
this.registerLogger.debug(`ADD DRIVE FILE: user ${user?.id ?? 'not set'}, name ${detectedName}, tmp ${path}`); 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) { if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
const isLocalUser = this.userEntityService.isLocalUser(user); const isLocalUser = this.userEntityService.isLocalUser(user);
const policies = await this.roleService.getUserPolicies(user.id); 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 driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
const maxFileSize = 1024 * 1024 * policies.maxFileSizeMb; 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 (maxFileSize < info.size) {
if (isLocalUser) { 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 usage limit exceeded
if (driveCapacity < usage + info.size) { if (driveCapacity < usage + info.size) {
if (isLocalUser) { if (isLocalUser) {

View File

@ -65,6 +65,7 @@ export type RolePolicies = {
canImportMuting: boolean; canImportMuting: boolean;
canImportUserLists: boolean; canImportUserLists: boolean;
chatAvailability: 'available' | 'readonly' | 'unavailable'; chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@ -101,6 +102,12 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportMuting: true, canImportMuting: true,
canImportUserLists: true, canImportUserLists: true,
chatAvailability: 'available', chatAvailability: 'available',
uploadableFileTypes: [
'text/plain',
'image/*',
'video/*',
'audio/*',
],
}; };
@Injectable() @Injectable()
@ -412,6 +419,15 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
chatAvailability: calc('chatAvailability', aggregateChatAvailability), chatAvailability: calc('chatAvailability', aggregateChatAvailability),
uploadableFileTypes: calc('uploadableFileTypes', vs => {
const set = new Set<string>();
for (const v of vs) {
for (const type of v) {
set.add(type);
}
}
return [...set];
}),
}; };
} }

View File

@ -228,6 +228,14 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
uploadableFileTypes: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
alwaysMarkNsfw: { alwaysMarkNsfw: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -110,6 +110,7 @@ export const ROLE_POLICIES = [
'canImportMuting', 'canImportMuting',
'canImportUserLists', 'canImportUserLists',
'chatAvailability', 'chatAvailability',
'uploadableFileTypes',
] as const; ] 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']; 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'];

View File

@ -406,6 +406,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </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'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix> <template #suffix>

View File

@ -146,6 +146,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</MkFolder> </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'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template> <template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
<template #suffix>{{ policies.alwaysMarkNsfw ? i18n.ts.yes : i18n.ts.no }}</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 { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';
import MkTextarea from '@/components/MkTextarea.vue';
const router = useRouter(); const router = useRouter();
const baseRoleQ = ref(''); const baseRoleQ = ref('');

View File

@ -39,6 +39,21 @@ export function uploadFile(file: File | Blob, options: {
const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => { const filePromise = new Promise<Misskey.entities.DriveFile>((resolve, reject) => {
if ($i == null) return 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))) { if ((file.size > instance.maxFileSize) || (file.size > ($i.policies.maxFileSizeMb * 1024 * 1024))) {
os.alert({ os.alert({
type: 'error', type: 'error',
@ -75,6 +90,12 @@ export function uploadFile(file: File | Blob, options: {
title: i18n.ts.failedToUpload, title: i18n.ts.failedToUpload,
text: i18n.ts.cannotUploadBecauseNoFreeSpace, 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 { } else {
os.alert({ os.alert({
type: 'error', type: 'error',

View File

@ -5264,6 +5264,7 @@ export type components = {
canHideAds: boolean; canHideAds: boolean;
driveCapacityMb: number; driveCapacityMb: number;
maxFileSizeMb: number; maxFileSizeMb: number;
uploadableFileTypes: string[];
alwaysMarkNsfw: boolean; alwaysMarkNsfw: boolean;
canUpdateBioMedia: boolean; canUpdateBioMedia: boolean;
pinLimit: number; pinLimit: number;