diff --git a/locales/index.d.ts b/locales/index.d.ts index dce1d15b8015..b0fbb1b90cc3 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": { /** * 遊び方 @@ -9739,6 +9748,10 @@ export interface Locale extends ILocale { * 通知の履歴をリセットする */ "flushNotification": string; + /** + * ドライブのファイルがセンシティブとして設定されました + */ + "sensitiveFlagAssigned": string; "_types": { /** * すべて diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index e5038ead054b..f3e6c0a6ce38 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: "遊び方" @@ -2560,6 +2562,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 000000000000..b3ca6df6cd3d --- /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 a68fc25ab2e8..9cfefe601055 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'); @@ -590,6 +592,7 @@ export class DriveService { if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; + if (file.isSensitiveByModerator) file.isSensitive = true; if (url !== null) { file.src = url; @@ -660,6 +663,7 @@ 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(); @@ -680,6 +684,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 }); @@ -689,7 +697,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) { @@ -699,6 +707,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 289f267c4b3a..3a261113de66 100644 --- a/packages/backend/src/core/entities/DriveFileEntityService.ts +++ b/packages/backend/src/core/entities/DriveFileEntityService.ts @@ -211,6 +211,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), @@ -247,6 +250,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 bd8f9a1cf7bd..b793d515c640 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 079e9cd9dc1a..6973e4d9d46a 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 4747b51b5fc7..ef783d2112af 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 ca88cc0e3975..3cc98058a29a 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 e682408977d1..160fcae42180 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 42c03204a420..099888922e94 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', @@ -90,6 +96,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } + if (!await this.roleService.isModerator(me) && file.isSensitiveByModerator) { + throw new ApiError(meta.errors.restrictedByModerator); + } + let packedFile; try { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 945eb27b54c9..f6f8db812458 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 6010fcee8a3c..e1a1396b24cf 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 87418961b7e2..ce14517ac7e0 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 e84958a69be8..6283391be5c3 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 8077edff5f44..bd690e1e7b9e 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