Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance: センシティブフラグ変更の由来をdrive_fileに持つ #15452

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### General
- Feat: アクセストークン発行時に通知するように
- Enhance: モデレーターによってセンシティブ指定されたファイルはユーザー側から非センシティブ化出来ないように ( #15443 )

### Client
- Feat: 投稿フォームで画像をプレビュー可能に
Expand Down
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,10 @@ export interface Locale extends ILocale {
* センシティブを解除する
*/
"unmarkAsSensitive": string;
/**
* モデレーターによってセンシティブとして設定されたため解除できません。
*/
"canNotUnmarkSensitive_markedByModerator": string;
/**
* ファイル名を入力
*/
Expand Down Expand Up @@ -10080,6 +10084,10 @@ export interface Locale extends ILocale {
* このページは、このファイルをアップロードしたユーザーしか閲覧できません。
*/
"thisPageCanBeSeenFromTheAuthor": string;
/**
* このファイルはモデレーターによりセンシティブとして設定されました。
*/
"setAsSensitiveByModerator": string;
};
"_externalResourceInstaller": {
/**
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ attachCancel: "添付取り消し"
deleteFile: "ファイルを削除"
markAsSensitive: "センシティブとして設定"
unmarkAsSensitive: "センシティブを解除する"
canNotUnmarkSensitive_markedByModerator: "モデレーターによってセンシティブとして設定されたため解除できません。"
enterFileName: "ファイル名を入力"
mute: "ミュート"
unmute: "ミュート解除"
Expand Down Expand Up @@ -2671,6 +2672,7 @@ _fileViewer:
uploadedAt: "追加日"
attachedNotes: "添付されているノート"
thisPageCanBeSeenFromTheAuthor: "このページは、このファイルをアップロードしたユーザーしか閲覧できません。"
setAsSensitiveByModerator: "このファイルはモデレーターによりセンシティブとして設定されました。"

_externalResourceInstaller:
title: "外部サイトからインストール"
Expand Down
27 changes: 27 additions & 0 deletions packages/backend/migration/1739257753979-sensitiveReason.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class SensitiveReason1739257753979 {
name = 'SensitiveReason1739257753979'

async up(queryRunner) {
await queryRunner.query(`
CREATE TYPE "public"."drive_file_sensitivechangereason_enum" AS ENUM('user', 'moderator', 'auto', 'none')
`);
await queryRunner.query(`
ALTER TABLE "drive_file"
ADD "sensitiveChangeReason" "public"."drive_file_sensitivechangereason_enum" NOT NULL DEFAULT 'none'
`);
}

async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "drive_file" DROP COLUMN "sensitiveChangeReason"
`);
await queryRunner.query(`
DROP TYPE "public"."drive_file_sensitivechangereason_enum"
`);
}
}
65 changes: 52 additions & 13 deletions packages/backend/src/core/DriveService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, Use
import type { Config } from '@/config.js';
import Logger from '@/logger.js';
import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { MiDriveFile } from '@/models/DriveFile.js';
import { DriveFileSensitiveReason, MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js';
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
Expand Down Expand Up @@ -89,7 +89,8 @@ type UploadFromUrlArgs = {
export class DriveService {
public static NoSuchFolderError = class extends Error {};
public static InvalidFileNameError = class extends Error {};
public static CannotUnmarkSensitiveError = class extends Error {};
public static CannotUnmarkSensitiveError_RestrictRole = class extends Error {};
public static CannotUnmarkSensitiveError_NsfwMarkedByModerator = class extends Error {};
private registerLogger: Logger;
private downloaderLogger: Logger;
private deleteLogger: Logger;
Expand Down Expand Up @@ -580,14 +581,23 @@ export class DriveService {
file.requestHeaders = requestHeaders;
file.maybeSensitive = info.sensitive;
file.maybePorn = info.porn;

file.sensitiveChangeReason = 'user';
file.isSensitive = user
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw ? true :
sensitive ?? false
? this.userEntityService.isLocalUser(user) && profile!.alwaysMarkNsfw
? true
: sensitive ?? false
: false;

if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) {
file.sensitiveChangeReason = 'auto';
file.isSensitive = true;
}
if (info.sensitive && this.meta.setSensitiveFlagAutomatically) {
file.sensitiveChangeReason = 'auto';
file.isSensitive = true;
}
if (userRoleNSFW) file.isSensitive = true;

if (url !== null) {
Expand Down Expand Up @@ -658,14 +668,28 @@ export class DriveService {

@bindThis
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;

if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
throw new DriveService.InvalidFileNameError();
}

if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && alwaysMarkNsfw && !values.isSensitive) {
throw new DriveService.CannotUnmarkSensitiveError();
const isChangeSensitive = values.isSensitive !== undefined && values.isSensitive !== file.isSensitive;
const isModerator = await this.roleService.isModerator(updater);
if (isChangeSensitive) {
// センシティブフラグの更新要求があるとき

if (!values.isSensitive) {
// センシティブフラグを解除するときの動き

if (!isModerator && file.sensitiveChangeReason === 'moderator') {
// モデレータ以外はモデレータによるセンシティブ解除を許可しない
throw new DriveService.CannotUnmarkSensitiveError_NsfwMarkedByModerator();
}

if ((await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw) {
// 常にセンシティブとするポリシーが設定されている場合、センシティブを解除できない
throw new DriveService.CannotUnmarkSensitiveError_RestrictRole();
}
}
}

if (values.folderId != null) {
Expand All @@ -679,7 +703,22 @@ export class DriveService {
}
}

await this.driveFilesRepository.update(file.id, values);
let sensitiveChangeReason: DriveFileSensitiveReason = file.sensitiveChangeReason;
if (isChangeSensitive) {
if (values.isSensitive) {
// モデレータではないがユーザIDが異なる場合は呼び出し元で弾かれてるはずなのでここでは考慮しない
sensitiveChangeReason = (isModerator && file.userId !== updater.id)
? 'moderator'
: 'user';
} else {
sensitiveChangeReason = 'none';
}
}

await this.driveFilesRepository.update(file.id, {
...values,
sensitiveChangeReason: sensitiveChangeReason,
});

const fileObj = await this.driveFileEntityService.pack(file.id, { self: true });

Expand All @@ -688,8 +727,8 @@ export class DriveService {
this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj);
}

if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) {
if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) {
if (isModerator && (file.userId !== updater.id)) {
if (isChangeSensitive) {
const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null;
if (values.isSensitive) {
this.moderationLogService.log(updater, 'markSensitiveDriveFile', {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/entities/DriveFileEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export class DriveFileEntityService {
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
sensitiveChangeReason: file.sensitiveChangeReason,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file),
Expand Down Expand Up @@ -241,6 +242,7 @@ export class DriveFileEntityService {
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
sensitiveChangeReason: file.sensitiveChangeReason,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file),
Expand Down
9 changes: 9 additions & 0 deletions packages/backend/src/models/DriveFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiDriveFolder } from './DriveFolder.js';

export const driveFileSensitiveReasons = ['user', 'moderator', 'auto', 'none'] as const;
export type DriveFileSensitiveReason = typeof driveFileSensitiveReasons[number];

@Entity('drive_file')
@Index(['userId', 'folderId', 'id'])
export class MiDriveFile {
Expand Down Expand Up @@ -155,6 +158,12 @@ export class MiDriveFile {
})
public isSensitive: boolean;

@Column('enum', {
enum: driveFileSensitiveReasons,
default: 'user',
})
public sensitiveChangeReason: DriveFileSensitiveReason;

@Index()
@Column('boolean', {
default: false,
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/models/json-schema/drive-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { driveFileSensitiveReasons } from '@/models/DriveFile.js';

export const packedDriveFileSchema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -42,6 +44,11 @@ export const packedDriveFileSchema = {
type: 'boolean',
optional: false, nullable: false,
},
sensitiveChangeReason: {
type: 'string',
enum: driveFileSensitiveReasons,
optional: false, nullable: false,
},
blurhash: {
type: 'string',
optional: false, nullable: true,
Expand Down
10 changes: 9 additions & 1 deletion packages/backend/src/server/api/endpoints/drive/files/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE',
id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7',
},

nsfwMarkedByModerator: {
message: 'Cannot be removed because it has been flagged as NSFW by a moderator.',
code: 'NSFW_MARKED_BY_MODERATOR',
id: 'c9ff65ab-c344-d715-f3c8-0c325f852951',
},
},
res: {
type: 'object',
Expand Down Expand Up @@ -103,8 +109,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidFileName);
} else if (e instanceof DriveService.NoSuchFolderError) {
throw new ApiError(meta.errors.noSuchFolder);
} else if (e instanceof DriveService.CannotUnmarkSensitiveError) {
} else if (e instanceof DriveService.CannotUnmarkSensitiveError_RestrictRole) {
throw new ApiError(meta.errors.restrictedByRole);
} else if (e instanceof DriveService.CannotUnmarkSensitiveError_NsfwMarkedByModerator) {
throw new ApiError(meta.errors.nsfwMarkedByModerator);
} else {
throw e;
}
Expand Down
1 change: 1 addition & 0 deletions packages/backend/test/unit/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('NoteCreateService', () => {
folderId: null,
folder: null,
isSensitive: false,
sensitiveChangeReason: 'user',
maybeSensitive: false,
maybePorn: false,
isLink: false,
Expand Down
15 changes: 15 additions & 0 deletions packages/frontend/src/components/MkPostFormAttaches.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu';
import { defaultStore } from '@/store';
import { copyToClipboard } from '@/scripts/copy-to-clipboard';
import { $i } from '@/account';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
Expand Down Expand Up @@ -95,11 +96,25 @@ function toggleSensitive(file) {
return;
}

if (!$i?.isModerator && file.isSensitive && file.sensitiveChangeReason === 'moderator') {
os.alert({
type: 'warning',
text: i18n.ts.canNotUnmarkSensitive_markedByModerator,
});
return;
}

misskeyApi('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
}).then(() => {
emit('changeSensitive', file, !file.isSensitive);
}).catch(err => {
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
});
}

Expand Down
14 changes: 11 additions & 3 deletions packages/frontend/src/pages/admin-file.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,17 @@ async function del() {
});
}

async function toggleIsSensitive(v) {
await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive.value = v;
async function toggleIsSensitive(v: boolean) {
await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v })
.then(() => isSensitive.value = v)
.catch(err => {
isSensitive.value = !v;
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
});
}

const headerActions = computed(() => [{
Expand Down
16 changes: 10 additions & 6 deletions packages/frontend/src/pages/drive.file.info.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo>{{ i18n.ts._fileViewer.thisPageCanBeSeenFromTheAuthor }}</MkInfo>
<MkLoading v-if="fetching"/>
<div v-else-if="file" class="_gaps">
<MkInfo v-if="file.isSensitive && file.sensitiveChangeReason === 'moderator'" warn>{{ i18n.ts._fileViewer.setAsSensitiveByModerator }}</MkInfo>
<div :class="$style.filePreviewRoot">
<MkMediaList :mediaList="[file]"></MkMediaList>
</div>
Expand Down Expand Up @@ -78,6 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { ref, computed, defineAsyncComponent, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import { $i } from '@/account';
import MkInfo from '@/components/MkInfo.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
Expand Down Expand Up @@ -156,17 +158,19 @@ function move() {
function toggleSensitive() {
if (!file.value) return;

if (!$i?.isModerator && file.value.isSensitive && file.value.sensitiveChangeReason === 'moderator') {
os.alert({
type: 'warning',
text: i18n.ts.canNotUnmarkSensitive_markedByModerator,
});
return;
}

os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
}).then(async () => {
await fetch();
}).catch(err => {
os.alert({
type: 'error',
title: i18n.ts.error,
text: err.message,
});
});
}

Expand Down
Loading
Loading