From abdaa186663f802ab60d72ef918ac56ac292ba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A6=E3=81=83?= =?UTF-8?q?=E3=83=BC=E3=80=82?= <56515516+mattyatea@users.noreply.github.com> Date: Tue, 18 Mar 2025 03:22:08 +0900 Subject: [PATCH] =?UTF-8?q?enhance(sensitive-flag):=E3=82=BB=E3=83=B3?= =?UTF-8?q?=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96=E3=83=95=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E3=81=AE=E6=A9=9F=E8=83=BD=E3=81=AE=E5=BC=B7=E5=8C=96=20(Missk?= =?UTF-8?q?eyIO#936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 13 +++++ locales/ja-JP.yml | 3 + .../migration/1739335129758-sensitiveFlag.js | 13 +++++ packages/backend/src/core/DriveService.ts | 19 ++++++- .../core/entities/DriveFileEntityService.ts | 6 ++ .../entities/NotificationEntityService.ts | 3 + packages/backend/src/models/DriveFile.ts | 6 ++ packages/backend/src/models/Notification.ts | 5 ++ .../src/models/json-schema/drive-file.ts | 4 ++ .../src/models/json-schema/notification.ts | 25 +++++++-- .../api/endpoints/drive/files/update.ts | 12 +++- packages/backend/src/types.ts | 2 + .../backend/test/unit/NoteCreateService.ts | 1 + .../src/components/MkNotification.vue | 55 +++++++++++++++++++ packages/frontend/src/const.ts | 1 + .../frontend/src/pages/drive.file.info.vue | 17 ++++-- packages/frontend/src/pages/note.vue | 15 +++++ packages/misskey-js/src/autogen/types.ts | 17 ++++-- 18 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 packages/backend/migration/1739335129758-sensitiveFlag.js diff --git a/locales/index.d.ts b/locales/index.d.ts index 269480fb0..4bc906b3c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5362,6 +5362,15 @@ export interface Locale extends ILocale { * {x}に投稿されます */ "willBePostedAt": ParameterizedString<"x">; + /** + * 管理者によって、ドライブのファイルがセンシティブとして設定されました。 + * 詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。 + */ + "sensitiveByModerator": string; + /** + * この情報は他のユーザーには公開されません。 + */ + "thisInfoIsNotVisibleOtherUser": string; "_bubbleGame": { /** * 遊び方 @@ -9747,6 +9756,10 @@ export interface Locale extends ILocale { * 通知の履歴をリセットする */ "flushNotification": string; + /** + * ドライブのファイルがセンシティブとして設定されました + */ + "sensitiveFlagAssigned": string; "_types": { /** * すべて diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 60507f0b1..1d1dc7857 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1334,6 +1334,8 @@ scheduled: "予約済み" unschedule: "予約を解除" setScheduledTime: "予約日時を設定" willBePostedAt: "{x}に投稿されます" +sensitiveByModerator: "管理者によって、ドライブのファイルがセンシティブとして設定されました。\n詳細については、[NSFWガイドライン](https://go.misskey.io/media-guideline)を確認してください。" +thisInfoIsNotVisibleOtherUser: "この情報は他のユーザーには公開されません。" _bubbleGame: howToPlay: "遊び方" @@ -2562,6 +2564,7 @@ _notification: renotedBySomeUsers: "{n}人がリノートしました" followedBySomeUsers: "{n}人にフォローされました" flushNotification: "通知の履歴をリセットする" + sensitiveFlagAssigned: "ドライブのファイルがセンシティブとして設定されました" _types: all: "すべて" diff --git a/packages/backend/migration/1739335129758-sensitiveFlag.js b/packages/backend/migration/1739335129758-sensitiveFlag.js new file mode 100644 index 000000000..b3ca6df6c --- /dev/null +++ b/packages/backend/migration/1739335129758-sensitiveFlag.js @@ -0,0 +1,13 @@ +export class SensitiveFlag1739335129758 { + name = 'SensitiveFlag1739335129758' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ADD "isSensitiveByModerator" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`CREATE INDEX "IDX_e779d1afdfa44dc3d64213cd2e" ON "drive_file" ("isSensitiveByModerator") `); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_e779d1afdfa44dc3d64213cd2e"`); + await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "isSensitiveByModerator"`); + } +} diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index a1d06a1d8..1a744f1b4 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -44,6 +44,7 @@ import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { NotificationService } from '@/core/NotificationService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -129,6 +130,7 @@ export class DriveService { private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, + private notificationService: NotificationService, ) { const logger = this.loggerService.getLogger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -664,13 +666,15 @@ export class DriveService { @bindThis public async updateFile(file: MiDriveFile, values: Partial, updater: MiUser) { const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw; + const isModerator = await this.roleService.isModerator(updater); 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(); + if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive && !values.isSensitive) { + if (alwaysMarkNsfw) throw new DriveService.CannotUnmarkSensitiveError(); + if (file.isSensitiveByModerator && (file.userId === updater.id)) throw new DriveService.CannotUnmarkSensitiveError(); } if (values.folderId != null) { @@ -684,6 +688,10 @@ export class DriveService { } } + if (isModerator && file.userId !== updater.id) { + values.isSensitiveByModerator = values.isSensitive; + } + await this.driveFilesRepository.update(file.id, values); const fileObj = await this.driveFileEntityService.pack(file.id, updater, { self: true }); @@ -693,7 +701,7 @@ export class DriveService { this.globalEventService.publishDriveStream(file.userId, 'fileUpdated', fileObj); } - if (await this.roleService.isModerator(updater) && (file.userId !== updater.id)) { + if (isModerator && (file.userId !== updater.id)) { if (values.isSensitive !== undefined && values.isSensitive !== file.isSensitive) { const user = file.userId ? await this.usersRepository.findOneByOrFail({ id: file.userId }) : null; if (values.isSensitive) { @@ -703,6 +711,11 @@ export class DriveService { fileUserUsername: user?.username ?? null, fileUserHost: user?.host ?? null, }); + if (file.userId) { + this.notificationService.createNotification(file.userId, 'sensitiveFlagAssigned', { + fileId: file.id, + }); + } } else { this.moderationLogService.log(updater, 'unmarkSensitiveDriveFile', { fileId: file.id, diff --git a/packages/backend/src/core/entities/DriveFileEntityService.ts b/packages/backend/src/core/entities/DriveFileEntityService.ts index 90e13153b..eb894326a 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -210,6 +210,9 @@ export class DriveFileEntityService { md5: file.md5, size: file.size, isSensitive: file.isSensitive, + ...(opts.detail ? { + isSensitiveByModerator: file.isSensitiveByModerator, + } : {}), blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), @@ -246,6 +249,9 @@ export class DriveFileEntityService { md5: file.md5, size: file.size, isSensitive: file.isSensitive, + ...(opts.detail ? { + isSensitiveByModerator: file.isSensitiveByModerator, + } : {}), blurhash: file.blurhash, properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file), diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index bd8f9a1cf..b793d515c 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -183,6 +183,9 @@ export class NotificationEntityService implements OnModuleInit { header: notification.customHeader, icon: notification.customIcon, } : {}), + ...(notification.type === 'sensitiveFlagAssigned' ? { + fileId: notification.fileId, + } : {}), }); } diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index 079e9cd9d..6973e4d9d 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -162,6 +162,12 @@ export class MiDriveFile { }) public isSensitive: boolean; + @Index() + @Column('boolean', { + default: false, + }) + public isSensitiveByModerator: boolean; + @Index() @Column('boolean', { default: false, diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 4747b51b5..ef783d211 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -93,6 +93,11 @@ export type MiNotification = { id: string; createdAt: string; draftId: MiScheduledNote['id']; +} | { + type: 'sensitiveFlagAssigned' + id: string; + fileId: string; + createdAt: string; } | { type: 'app'; id: string; diff --git a/packages/backend/src/models/json-schema/drive-file.ts b/packages/backend/src/models/json-schema/drive-file.ts index ca88cc0e3..3cc98058a 100644 --- a/packages/backend/src/models/json-schema/drive-file.ts +++ b/packages/backend/src/models/json-schema/drive-file.ts @@ -42,6 +42,10 @@ export const packedDriveFileSchema = { type: 'boolean', optional: false, nullable: false, }, + isSensitiveByModerator: { + type: 'boolean', + optional: true, nullable: true, + }, blurhash: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index e68240897..160fcae42 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -309,8 +309,8 @@ export const packedNotificationSchema = { type: 'object', ref: 'NoteDraft', optional: false, nullable: false, - } - } + }, + }, }, { type: 'object', properties: { @@ -324,8 +324,8 @@ export const packedNotificationSchema = { type: 'object', ref: 'Note', optional: false, nullable: false, - } - } + }, + }, }, { type: 'object', properties: { @@ -339,8 +339,21 @@ export const packedNotificationSchema = { type: 'object', ref: 'NoteDraft', optional: false, nullable: false, - } - } + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['sensitiveFlagAssigned'], + }, + fileId: { + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 42c03204a..1a03827ec 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -51,6 +51,12 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7', }, + + restrictedByModerator: { + message: 'The isSensitive specified by the administrator cannot be changed.', + code: 'RESTRICTED_BY_ADMINISTRATOR', + id: '20e6c501-e579-400d-97e4-1c7efc286f35', + }, }, res: { type: 'object', @@ -105,7 +111,11 @@ export default class extends Endpoint { // eslint- } else if (e instanceof DriveService.NoSuchFolderError) { throw new ApiError(meta.errors.noSuchFolder); } else if (e instanceof DriveService.CannotUnmarkSensitiveError) { - throw new ApiError(meta.errors.restrictedByRole); + if (file.isSensitiveByModerator) { + throw new ApiError(meta.errors.restrictedByModerator); + } else { + throw new ApiError(meta.errors.restrictedByRole); + } } else { throw e; } diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 945eb27b5..f6f8db812 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -26,6 +26,7 @@ import type { MiNote } from '@/models/Note.js'; * noteScheduled - 予約投稿が予約された * scheduledNotePosted - 予約投稿が投稿された * scheduledNoteError - 予約投稿がエラーになった + * sensitiveFlagAssigned - センシティブフラグが付与された * app - アプリ通知 * test - テスト通知(サーバー側) */ @@ -45,6 +46,7 @@ export const notificationTypes = [ 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError', + 'sensitiveFlagAssigned', 'app', 'test', ] as const; diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts index 6010fcee8..e1a1396b2 100644 --- a/packages/backend/test/unit/NoteCreateService.ts +++ b/packages/backend/test/unit/NoteCreateService.ts @@ -95,6 +95,7 @@ describe('NoteCreateService', () => { folderId: null, folder: null, isSensitive: false, + isSensitiveByModerator: false, maybeSensitive: false, maybePorn: false, isLink: false, diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 87418961b..ce14517ac 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -8,6 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ +
+
@@ -71,6 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }} {{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }} {{ notification.header }} + {{ i18n.ts._notification.sensitiveFlagAssigned }}
@@ -159,6 +168,10 @@ SPDX-License-Identifier: AGPL-3.0-only
+ + + {{ i18n.ts.thisInfoIsNotVisibleOtherUser }} + @@ -341,6 +354,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_sensitiveFlagAssigned { + padding: 3px; + background: var(--eventOther); + pointer-events: none; +} + .tail { flex: 1; min-width: 0; @@ -430,6 +449,42 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) color: #fff; } +.iconFrame { + position: relative; + width: 100%; + height: 100%; + padding: 4px; + border-radius: 100%; + box-sizing: border-box; + pointer-events: none; + user-select: none; + filter: drop-shadow(0px 2px 2px #00000044); + box-shadow: 0 1px 0px #ffffff88 inset; + overflow: clip; + background: linear-gradient(0deg, #703827, #d37566); +} + +.iconImg { + width: calc(100% - 12px); + height: calc(100% - 12px); + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + filter: drop-shadow(0px 1px 2px #000000aa); +} + +.iconInner { + position: relative; + width: 100%; + height: 100%; + border-radius: 100%; + box-shadow: 0 1px 0px #ffffff88 inset; + background: linear-gradient(0deg, #d37566, #703827); +} + @container (max-width: 600px) { .root { padding: 16px; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index e84958a69..6283391be 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -70,6 +70,7 @@ export const notificationTypes = [ 'noteScheduled', 'scheduledNotePosted', 'scheduledNoteError', + 'sensitiveFlagAssigned', 'app', ] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue index 8077edff5..bd690e1e7 100644 --- a/packages/frontend/src/pages/drive.file.info.vue +++ b/packages/frontend/src/pages/drive.file.info.vue @@ -6,6 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only