From 93869a5f34386a7bd6e99df779150733fb1730c4 Mon Sep 17 00:00:00 2001 From: Mar0xy Date: Tue, 5 Dec 2023 22:19:53 +0100 Subject: [PATCH] add: mark instance as NSFW Closes transfem-org/Sharkey#197 --- .../migration/1701809447000-NSFW-Instance.js | 11 ++++++ .../core/activitypub/models/ApImageService.ts | 13 ++++++- .../core/entities/InstanceEntityService.ts | 1 + packages/backend/src/models/Instance.ts | 5 +++ .../models/json-schema/federation-instance.ts | 5 +++ .../admin/federation/update-instance.ts | 39 ++++++++++++------- .../api/endpoints/federation/instances.ts | 9 +++++ .../frontend/src/pages/about.federation.vue | 3 ++ .../frontend/src/pages/admin/federation.vue | 3 ++ packages/frontend/src/pages/instance-info.vue | 11 ++++++ packages/misskey-js/src/entities.ts | 1 + 11 files changed, 85 insertions(+), 16 deletions(-) create mode 100644 packages/backend/migration/1701809447000-NSFW-Instance.js diff --git a/packages/backend/migration/1701809447000-NSFW-Instance.js b/packages/backend/migration/1701809447000-NSFW-Instance.js new file mode 100644 index 0000000000..882aa9865d --- /dev/null +++ b/packages/backend/migration/1701809447000-NSFW-Instance.js @@ -0,0 +1,11 @@ +export class NSFWInstance1701809447000 { + name = 'NSFWInstance1701809447000' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "isNSFW" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "isNSFW"`); + } +} diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index a4cd533892..b7b8acd661 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, InstancesRepository } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { MetaService } from '@/core/MetaService.js'; @@ -18,6 +18,7 @@ import { checkHttps } from '@/misc/check-https.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import type { IObject } from '../type.js'; +import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class ApImageService { @@ -27,10 +28,14 @@ export class ApImageService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.instancesRepository) + private instancesRepository: InstancesRepository, + private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.logger = this.apLoggerService.logger; } @@ -68,6 +73,12 @@ export class ApImageService { // 2. or the image is not sensitive const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + const shouldBeSensitive = await this.instancesRepository.findOneBy({ host: this.utilityService.toPuny(actor.host), isNSFW: true }); + + if (shouldBeSensitive) { + image.sensitive = true; + } + const file = await this.driveService.uploadFromUrl({ url: image.url, user: actor, diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 7d16a7a80e..515b356dee 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -48,6 +48,7 @@ export class InstanceEntityService { themeColor: instance.themeColor, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, + isNSFW: instance.isNSFW, }; } diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index b225d918d6..4200b1b461 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -144,4 +144,9 @@ export class MiInstance { nullable: true, }) public infoUpdatedAt: Date | null; + + @Column('boolean', { + default: false, + }) + public isNSFW: boolean; } diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 442e1076f2..ac4b37fb57 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -108,5 +108,10 @@ export const packedFederationInstanceSchema = { optional: false, nullable: true, format: 'date-time', }, + isNSFW: { + type: 'boolean', + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts index 357bf83e87..4db52b1052 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts @@ -23,8 +23,9 @@ export const paramDef = { properties: { host: { type: 'string' }, isSuspended: { type: 'boolean' }, + isNSFW: { type: 'boolean' }, }, - required: ['host', 'isSuspended'], + required: ['host'], } as const; @Injectable() @@ -44,23 +45,31 @@ export default class extends Endpoint { // eslint- throw new Error('instance not found'); } - await this.federatedInstanceService.update(instance.id, { - isSuspended: ps.isSuspended, - }); + if (ps.isSuspended != null) { + await this.federatedInstanceService.update(instance.id, { + isSuspended: ps.isSuspended, + }); - if (instance.isSuspended !== ps.isSuspended) { - if (ps.isSuspended) { - this.moderationLogService.log(me, 'suspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); - } else { - this.moderationLogService.log(me, 'unsuspendRemoteInstance', { - id: instance.id, - host: instance.host, - }); + if (instance.isSuspended !== ps.isSuspended) { + if (ps.isSuspended) { + this.moderationLogService.log(me, 'suspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } else { + this.moderationLogService.log(me, 'unsuspendRemoteInstance', { + id: instance.id, + host: instance.host, + }); + } } } + + if (ps.isNSFW != null) { + await this.federatedInstanceService.update(instance.id, { + isNSFW: ps.isNSFW, + }); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c8beefa9c7..e143dcfe89 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -40,6 +40,7 @@ export const paramDef = { federating: { type: 'boolean', nullable: true }, subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, + nsfw: { type: 'boolean', nullable: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, offset: { type: 'integer', default: 0 }, sort: { type: 'string' }, @@ -103,6 +104,14 @@ export default class extends Endpoint { // eslint- } } + if (typeof ps.nsfw === 'boolean') { + if (ps.nsfw) { + query.andWhere('instance.isNSFW = TRUE'); + } else { + query.andWhere('instance.isNSFW = FALSE'); + } + } + if (typeof ps.silenced === "boolean") { const meta = await this.metaService.fetch(true); diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index db4fff4e37..27f7784007 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -78,6 +79,7 @@ const pagination = { state === 'blocked' ? { blocked: true } : state === 'silenced' ? { silenced: true } : state === 'notResponding' ? { notResponding: true } : + state === 'nsfw' ? { nsfw: true } : {}), })), } as Paging; @@ -87,6 +89,7 @@ function getStatus(instance) { if (instance.isBlocked) return 'Blocked'; if (instance.isSilenced) return 'Silenced'; if (instance.isNotResponding) return 'Error'; + if (instance.isNSFW) return 'NSFW'; return 'Alive'; } diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 39285deb13..e09d5181c5 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -86,6 +87,7 @@ const pagination = { state === 'blocked' ? { blocked: true } : state === 'silenced' ? { silenced: true } : state === 'notResponding' ? { notResponding: true } : + state === 'nsfw' ? { nsfw: true } : {}), })), }; @@ -95,6 +97,7 @@ function getStatus(instance) { if (instance.isBlocked) return 'Blocked'; if (instance.isSilenced) return 'Silenced'; if (instance.isNotResponding) return 'Error'; + if (instance.isNSFW) return 'NSFW'; return 'Alive'; } diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 51e6939c2d..668e4e61bf 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.stopActivityDelivery }} {{ i18n.ts.blockThisInstance }} {{ i18n.ts.silenceThisInstance }} + Mark as NSFW Refresh metadata @@ -149,6 +150,7 @@ let instance = $ref(null); let suspended = $ref(false); let isBlocked = $ref(false); let isSilenced = $ref(false); +let isNSFW = $ref(false); let faviconUrl = $ref(null); const usersPagination = { @@ -172,6 +174,7 @@ async function fetch(): Promise { suspended = instance.isSuspended; isBlocked = instance.isBlocked; isSilenced = instance.isSilenced; + isNSFW = instance.isNSFW; faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview'); } @@ -201,6 +204,14 @@ async function toggleSuspend(): Promise { }); } +async function toggleNSFW(): Promise { + if (!instance) throw new Error('No instance?'); + await os.api('admin/federation/update-instance', { + host: instance.host, + isNSFW: isNSFW, + }); +} + function refreshMetadata(): void { if (!instance) throw new Error('No instance?'); os.api('admin/federation/refresh-remote-instance-metadata', { diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 05960a5719..1a4509cabf 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -609,6 +609,7 @@ export type Instance = { faviconUrl: string | null; themeColor: string | null; infoUpdatedAt: DateString | null; + isNSFW: boolean; }; export type Signin = {