diff --git a/locales/en-US.yml b/locales/en-US.yml index 8d2c52d7f..24e07bc31 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -197,6 +197,7 @@ perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" silenceThisInstance: "Silence this instance" +sensitiveMediaThisInstance: "Mark media from this instance as sensitive" operations: "Operations" software: "Software" version: "Version" @@ -218,6 +219,8 @@ blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." silencedInstances: "Silenced instances" silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances." +sensitiveMediaInstances: "Instances with sensitive media" +sensitiveMediaInstancesDescription: "List the hostnames of the instances that you want to mark all media from this instance as sensitive." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" diff --git a/locales/index.d.ts b/locales/index.d.ts index fcece45ed..8c178a3db 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -204,6 +204,7 @@ export interface Locale { "stopActivityDelivery": string; "blockThisInstance": string; "silenceThisInstance": string; + "sensitiveMediaThisInstance": string; "operations": string; "software": string; "version": string; @@ -225,6 +226,8 @@ export interface Locale { "blockedInstancesDescription": string; "silencedInstances": string; "silencedInstancesDescription": string; + "sensitiveMediaInstances": string; + "sensitiveMediaInstancesDescription": string; "muteAndBlock": string; "mutedUsers": string; "blockedUsers": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7c3967059..38f5a353b 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -201,6 +201,7 @@ perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" blockThisInstance: "このサーバーをブロック" silenceThisInstance: "サーバーをサイレンス" +sensitiveMediaThisInstance: "このサーバーのメディアを全てセンシティブとして設定" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -222,6 +223,8 @@ blockedInstances: "ブロックしたサーバー" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" silencedInstances: "サイレンスしたサーバー" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" +sensitiveMediaInstances: "センシティブなメディアを含むサーバー" +sensitiveMediaInstancesDescription: "センシティブなメディアを含むサーバーのホストを改行で区切って設定します。このサーバーからのメディアは全てセンシティブとして扱われます。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 698ccf8e9..00c78cf4f 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -201,6 +201,7 @@ perDay: "1일마다" stopActivityDelivery: "액티비티 보내지 않기" blockThisInstance: "이 서버를 차단" silenceThisInstance: "서버를 사일런스" +sensitiveMediaThisInstance: "이 서버의 미디어를 모두 민감한 미디어로 표시" operations: "작업" software: "소프트웨어" version: "버전" @@ -222,6 +223,8 @@ blockedInstances: "차단된 서버" blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." silencedInstances: "사일런스한 서버" silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다." +sensitiveMediaInstances: "민감한 미디어를 포함한 서버" +sensitiveMediaInstancesDescription: "민감한 미디어를 포함한 서버의 호스트명을 한 줄에 하나씩 입력합니다. 이 서버에 소속된 유저가 업로드한 미디어는 모두 민감한 미디어로 표시됩니다." muteAndBlock: "뮤트 및 차단" mutedUsers: "뮤트한 유저" blockedUsers: "차단한 유저" diff --git a/packages/backend/migration/1704622962215-sensitive-media-hosts.js b/packages/backend/migration/1704622962215-sensitive-media-hosts.js new file mode 100644 index 000000000..9b433d71c --- /dev/null +++ b/packages/backend/migration/1704622962215-sensitive-media-hosts.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SensitiveMediaHosts1704622962215 { + name = 'SensitiveMediaHosts1704622962215' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveMediaHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveMediaHosts"`); + } +} diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 5dec36c89..e971e9f26 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -42,6 +42,12 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isSensitiveMediaHost(sensitiveMediaHosts: string[] | undefined, host: string | null): boolean { + if (!sensitiveMediaHosts || host == null) return false; + return sensitiveMediaHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); + } + @bindThis public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { if (sensitiveWords.length === 0) return false; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index e3eccd540..250a4439e 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -173,6 +173,9 @@ export class ApNoteService { const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apHashtags = extractApHashtags(note.tag); + const meta = await this.metaService.fetch(); + const isSensitiveMediaHost = this.utilityService.isSensitiveMediaHost(meta.blockedHosts, this.utilityService.extractDbHost(note.id ?? entryUri)); + // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない @@ -180,7 +183,7 @@ export class ApNoteService { const files = (await Promise.all(toArray(note.attachment).map(attach => ( limit(() => this.apImageService.resolveImage(actor, { ...attach, - sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする + sensitive: isSensitiveMediaHost || note.sensitive, // Noteがsensitiveなら添付もsensitiveにする })) )))); diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index f05a414d8..119eec54d 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -43,6 +43,7 @@ export class InstanceEntityService { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), + isSensitiveMedia: this.utilityService.isSensitiveMediaHost(meta.sensitiveMediaHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 06d9ae856..3b9feef6c 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -81,6 +81,11 @@ export class MiMeta { }) public silencedHosts: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public sensitiveMediaHosts: string[]; + @Column('varchar', { length: 1024, nullable: true, diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 341731427..6579238d8 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -83,6 +83,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isSensitiveMedia: { + type: 'boolean', + optional: false, nullable: false, + }, iconUrl: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index a9deb0133..340a2700f 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -116,6 +116,16 @@ export const meta = { nullable: false, }, }, + sensitiveMediaHosts: { + type: 'array', + optional: true, + nullable: false, + items: { + type: 'string', + optional: false, + nullable: false, + }, + }, pinnedUsers: { type: 'array', optional: false, nullable: false, @@ -491,6 +501,7 @@ export default class extends Endpoint { // eslint- hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, + sensitiveMediaHosts: instance.sensitiveMediaHosts, sensitiveWords: instance.sensitiveWords, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 689ef89e5..e401d7c26 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -138,6 +138,11 @@ export const paramDef = { type: 'string', }, }, + sensitiveMediaHosts: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, urlPreviewDenyList: { type: 'array', nullable: true, items: { type: 'string', } }, @@ -173,13 +178,23 @@ export default class extends Endpoint { // eslint- if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } + if (Array.isArray(ps.silencedHosts)) { let lastValue = ''; set.silencedHosts = ps.silencedHosts.sort().filter((h) => { const lv = lastValue; lastValue = h; return h !== '' && h !== lv && !set.blockedHosts?.includes(h); - }); + }).map(x => x.toLowerCase()); + } + + if (Array.isArray(ps.sensitiveMediaHosts)) { + let lastValue = ''; + set.sensitiveMediaHosts = ps.sensitiveMediaHosts.sort().filter((h) => { + const lv = lastValue; + lastValue = h; + return h !== '' && h !== lv && !set.blockedHosts?.includes(h); + }).map(x => x.toLowerCase()); } if (Array.isArray(ps.urlPreviewDenyList)) { diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 356eca2af..590cf3f80 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.silencedInstances }} + + {{ i18n.ts.sensitiveMediaInstances }} + + {{ i18n.ts.save }} @@ -35,18 +39,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const blockedHosts = ref(''); const silencedHosts = ref(''); +const sensitiveMediaHosts = ref(''); const tab = ref('block'); async function init() { const meta = await os.api('admin/meta'); blockedHosts.value = meta.blockedHosts.join('\n'); silencedHosts.value = meta.silencedHosts.join('\n'); + sensitiveMediaHosts.value = meta.sensitiveMediaHosts.join('\n'); } function save() { os.apiWithDialog('admin/update-meta', { blockedHosts: blockedHosts.value.split('\n') || [], silencedHosts: silencedHosts.value.split('\n') || [], + sensitiveMediaHosts: sensitiveMediaHosts.value.split('\n') || [], }).then(() => { fetchInstance(); @@ -63,6 +70,10 @@ const headerTabs = computed(() => [{ key: 'silence', title: i18n.ts.silence, icon: 'ti ti-eye-off', +}, { + key: 'sensitive', + title: i18n.ts.sensitive, + icon: 'ti ti-photo-exclamation', }]); definePageMetadata({ diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 97dc0a863..f09df07db 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 }} + {{ i18n.ts.sensitiveMediaThisInstance }} Refresh metadata @@ -149,6 +150,7 @@ const instance = ref(null); const suspended = ref(false); const isBlocked = ref(false); const isSilenced = ref(false); +const isSensitiveMedia = ref(false); const faviconUrl = ref(null); const usersPagination = { @@ -172,6 +174,7 @@ async function fetch(): Promise { suspended.value = instance.value?.isSuspended ?? false; isBlocked.value = instance.value?.isBlocked ?? false; isSilenced.value = instance.value?.isSilenced ?? false; + isSensitiveMedia.value = instance.value?.isSensitiveMedia ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); } @@ -194,6 +197,16 @@ async function toggleSilenced(): Promise { }); } +async function toggleSensitiveMedia(): Promise { + if (!meta.value) throw new Error('No meta?'); + if (!instance.value) throw new Error('No instance?'); + const { host } = instance.value; + const sensitiveMediaHosts = meta.value.sensitiveMediaHosts ?? []; + await os.api('admin/update-meta', { + sensitiveMediaHosts: isSensitiveMedia.value ? sensitiveMediaHosts.concat([host]) : sensitiveMediaHosts.filter(x => x !== host), + }); +} + async function toggleSuspend(): Promise { if (!instance.value) throw new Error('No instance?'); await os.api('admin/federation/update-instance', { diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 03b17ba97..fc083347a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2023.12.2-io - * generatedAt: 2023-12-28T08:11:12.906Z + * version: 2023.11.1-io.3a + * generatedAt: 2024-01-07T10:20:39.681Z */ /** @@ -4209,6 +4209,7 @@ export type components = { maintainerName: string | null; maintainerEmail: string | null; isSilenced: boolean; + isSensitiveMedia: boolean; /** Format: url */ iconUrl: string | null; /** Format: url */ @@ -4560,6 +4561,7 @@ export type operations = { enableServiceWorker: boolean; translatorAvailable: boolean; silencedHosts?: string[]; + sensitiveMediaHosts?: string[]; pinnedUsers: string[]; hiddenTags: string[]; blockedHosts: string[]; @@ -8676,6 +8678,7 @@ export type operations = { perUserListTimelineCacheMax?: number; notesPerOneAd?: number; silencedHosts?: string[] | null; + sensitiveMediaHosts?: string[] | null; urlPreviewDenyList?: string[] | null; }; }; @@ -25241,7 +25244,7 @@ export type operations = { * @default other * @enum {string} */ - category?: 'nsfw' | 'spam' | 'explicit' | 'phishing' | 'personalInfoLeak' | 'selfHarm' | 'criticalBreach' | 'otherBreach' | 'violationRights' | 'violationRightsOther' | 'other' | 'personalinfoleak' | 'selfharm' | 'criticalbreach' | 'otherbreach' | 'violationrights' | 'violationrightsother' | 'notlike'; + category?: 'nsfw' | 'spam' | 'explicit' | 'phishing' | 'personalInfoLeak' | 'selfHarm' | 'criticalBreach' | 'otherBreach' | 'violationRights' | 'violationRightsOther' | 'other'; }; }; };