ロールにNSFWを強制的につけるオプションを追加 (#10731)
* ロールにNSFWを強制的につけるオプションを追加 * すでにあるファイルにNSFWが付与できない * NSFWを付与しようとするとエラーに * add test * Update packages/backend/src/core/RoleService.ts Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> * spacingで怒られたので * ロール作成時のプロパティ削除 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
parent
be226ec187
commit
2d84e04240
13 changed files with 144 additions and 6 deletions
|
@ -32,6 +32,8 @@
|
||||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||||
- 管理者が予約ユーザー名を設定できるようになりました。
|
- 管理者が予約ユーザー名を設定できるようになりました。
|
||||||
- Fix: フォローリクエストの通知が残る問題を修正
|
- Fix: フォローリクエストの通知が残る問題を修正
|
||||||
|
- ロールに強制的にNSFWを付与する設定を追加
|
||||||
|
* アップロード済みのファイルはNSFWにならない為注意してください。
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- チャンネル内検索ができるように
|
- チャンネル内検索ができるように
|
||||||
|
|
|
@ -1331,6 +1331,7 @@ _role:
|
||||||
canInvite: "サーバー招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
antennaMax: "アンテナの作成可能数"
|
antennaMax: "アンテナの作成可能数"
|
||||||
wordMuteMax: "ワードミュートの最大文字数"
|
wordMuteMax: "ワードミュートの最大文字数"
|
||||||
|
|
|
@ -449,7 +449,12 @@ export class DriveService {
|
||||||
}: AddFileArgs): Promise<DriveFile> {
|
}: AddFileArgs): Promise<DriveFile> {
|
||||||
let skipNsfwCheck = false;
|
let skipNsfwCheck = false;
|
||||||
const instance = await this.metaService.fetch();
|
const instance = await this.metaService.fetch();
|
||||||
if (user == null) skipNsfwCheck = true;
|
const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw;
|
||||||
|
if (user == null) {
|
||||||
|
skipNsfwCheck = true;
|
||||||
|
} else if (userRoleNSFW) {
|
||||||
|
skipNsfwCheck = true;
|
||||||
|
}
|
||||||
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true;
|
||||||
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
|
if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true;
|
||||||
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true;
|
||||||
|
@ -571,6 +576,7 @@ export class DriveService {
|
||||||
|
|
||||||
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
|
||||||
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
|
||||||
|
if (userRoleNSFW) file.isSensitive = true;
|
||||||
|
|
||||||
if (url !== null) {
|
if (url !== null) {
|
||||||
file.src = url;
|
file.src = url;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export type RolePolicies = {
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
driveCapacityMb: number;
|
driveCapacityMb: number;
|
||||||
|
alwaysMarkNsfw: boolean;
|
||||||
pinLimit: number;
|
pinLimit: number;
|
||||||
antennaLimit: number;
|
antennaLimit: number;
|
||||||
wordMuteLimit: number;
|
wordMuteLimit: number;
|
||||||
|
@ -45,6 +46,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
driveCapacityMb: 100,
|
driveCapacityMb: 100,
|
||||||
|
alwaysMarkNsfw: false,
|
||||||
pinLimit: 5,
|
pinLimit: 5,
|
||||||
antennaLimit: 5,
|
antennaLimit: 5,
|
||||||
wordMuteLimit: 200,
|
wordMuteLimit: 200,
|
||||||
|
@ -279,6 +281,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||||
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
|
||||||
|
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
|
||||||
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
|
||||||
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
|
||||||
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
|
||||||
|
|
|
@ -40,8 +40,13 @@ export const meta = {
|
||||||
code: 'NO_SUCH_FOLDER',
|
code: 'NO_SUCH_FOLDER',
|
||||||
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
|
id: 'ea8fb7a5-af77-4a08-b608-c0218176cd73',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
|
restrictedByRole: {
|
||||||
|
message: 'This feature is restricted by your role.',
|
||||||
|
code: 'RESTRICTED_BY_ROLE',
|
||||||
|
id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
|
||||||
|
},
|
||||||
|
},
|
||||||
res: {
|
res: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -77,7 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||||
|
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(me.id)).alwaysMarkNsfw;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw new ApiError(meta.errors.noSuchFile);
|
throw new ApiError(meta.errors.noSuchFile);
|
||||||
}
|
}
|
||||||
|
@ -93,6 +98,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
if (ps.comment !== undefined) file.comment = ps.comment;
|
if (ps.comment !== undefined) file.comment = ps.comment;
|
||||||
|
|
||||||
|
if (ps.isSensitive !== undefined && ps.isSensitive !== file.isSensitive && alwaysMarkNsfw && !ps.isSensitive) {
|
||||||
|
throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
|
if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive;
|
||||||
|
|
||||||
if (ps.folderId !== undefined) {
|
if (ps.folderId !== undefined) {
|
||||||
|
|
|
@ -93,6 +93,12 @@ export const meta = {
|
||||||
code: 'FORBIDDEN_TO_SET_YOURSELF',
|
code: 'FORBIDDEN_TO_SET_YOURSELF',
|
||||||
id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
|
id: '25c90186-4ab0-49c8-9bba-a1fa6c202ba4',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
restrictedByRole: {
|
||||||
|
message: 'This feature is restricted by your role.',
|
||||||
|
code: 'RESTRICTED_BY_ROLE',
|
||||||
|
id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
@ -239,7 +245,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
|
||||||
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||||
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
|
||||||
if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
if (typeof ps.alwaysMarkNsfw === 'boolean') {
|
||||||
|
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
|
||||||
|
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
|
||||||
|
}
|
||||||
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
|
||||||
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
|
||||||
|
|
||||||
|
|
|
@ -352,6 +352,72 @@ describe('Note', () => {
|
||||||
assert.strictEqual(myNote.renote.reply.files.length, 1);
|
assert.strictEqual(myNote.renote.reply.files.length, 1);
|
||||||
assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id);
|
assert.strictEqual(myNote.renote.reply.files[0].id, file.body.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('NSFWが強制されている場合変更できない', async () => {
|
||||||
|
const file = await uploadFile(alice);
|
||||||
|
|
||||||
|
const res = await api('admin/roles/create', {
|
||||||
|
name: 'test',
|
||||||
|
description: '',
|
||||||
|
color: null,
|
||||||
|
iconUrl: null,
|
||||||
|
displayOrder: 0,
|
||||||
|
target: 'manual',
|
||||||
|
condFormula: {},
|
||||||
|
isAdministrator: false,
|
||||||
|
isModerator: false,
|
||||||
|
isPublic: false,
|
||||||
|
isExplorable: false,
|
||||||
|
asBadge: false,
|
||||||
|
canEditMembersByModerator: false,
|
||||||
|
policies: {
|
||||||
|
alwaysMarkNsfw: {
|
||||||
|
useDefault: false,
|
||||||
|
priority: 0,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
|
||||||
|
const assign = await api('admin/roles/assign', {
|
||||||
|
userId: alice.id,
|
||||||
|
roleId: res.body.id,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(assign.status, 204);
|
||||||
|
assert.strictEqual(file.body.isSensitive, false);
|
||||||
|
|
||||||
|
const nsfwfile = await uploadFile(alice);
|
||||||
|
|
||||||
|
assert.strictEqual(nsfwfile.status, 200);
|
||||||
|
assert.strictEqual(nsfwfile.body.isSensitive, true);
|
||||||
|
|
||||||
|
const liftnsfw = await api('drive/files/update', {
|
||||||
|
fileId: nsfwfile.body.id,
|
||||||
|
isSensitive: false,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(liftnsfw.status, 400);
|
||||||
|
assert.strictEqual(liftnsfw.body.error.code, 'RESTRICTED_BY_ROLE');
|
||||||
|
|
||||||
|
const oldaddnsfw = await api('drive/files/update', {
|
||||||
|
fileId: file.body.id,
|
||||||
|
isSensitive: true,
|
||||||
|
}, alice);
|
||||||
|
|
||||||
|
assert.strictEqual(oldaddnsfw.status, 200);
|
||||||
|
|
||||||
|
await api('admin/roles/unassign', {
|
||||||
|
userId: alice.id,
|
||||||
|
roleId: res.body.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await api('admin/roles/delete', {
|
||||||
|
roleId: res.body.id,
|
||||||
|
}, alice);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('notes/create', () => {
|
describe('notes/create', () => {
|
||||||
|
|
|
@ -61,6 +61,7 @@ export const ROLE_POLICIES = [
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
'driveCapacityMb',
|
'driveCapacityMb',
|
||||||
|
'alwaysMarkNsfw',
|
||||||
'pinLimit',
|
'pinLimit',
|
||||||
'antennaLimit',
|
'antennaLimit',
|
||||||
'wordMuteLimit',
|
'wordMuteLimit',
|
||||||
|
|
|
@ -231,6 +231,26 @@
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.alwaysMarkNsfw, 'alwaysMarkNsfw'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.alwaysMarkNsfw }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.alwaysMarkNsfw.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.alwaysMarkNsfw.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.alwaysMarkNsfw)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.alwaysMarkNsfw.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.alwaysMarkNsfw.value" :disabled="role.policies.alwaysMarkNsfw.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.alwaysMarkNsfw.priority" :min="0" :max="2" :step="1" easing :text-converter="(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.pinMax, 'pinLimit'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -75,6 +75,14 @@
|
||||||
</MkInput>
|
</MkInput>
|
||||||
</MkFolder>
|
</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>
|
||||||
|
<MkSwitch v-model="policies.alwaysMarkNsfw">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
|
||||||
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
|
||||||
<template #suffix>{{ policies.pinLimit }}</template>
|
<template #suffix>{{ policies.pinLimit }}</template>
|
||||||
|
|
|
@ -119,6 +119,13 @@ function saveProfile() {
|
||||||
os.api('i/update', {
|
os.api('i/update', {
|
||||||
alwaysMarkNsfw: !!alwaysMarkNsfw,
|
alwaysMarkNsfw: !!alwaysMarkNsfw,
|
||||||
autoSensitive: !!autoSensitive,
|
autoSensitive: !!autoSensitive,
|
||||||
|
}).catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.error,
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
alwaysMarkNsfw = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,12 @@ function toggleSensitive(file: Misskey.entities.DriveFile) {
|
||||||
os.api('drive/files/update', {
|
os.api('drive/files/update', {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
isSensitive: !file.isSensitive,
|
isSensitive: !file.isSensitive,
|
||||||
|
}).catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
title: i18n.ts.error,
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue