feat(federation): 特定の連合サーバーのメディアを全てセンシティブとして設定する機能を追加 (MisskeyIO#340)

This commit is contained in:
まっちゃとーにゅ 2024-01-07 19:51:07 +09:00 committed by GitHub
parent ce58adce22
commit d4a8e9a499
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 105 additions and 5 deletions

View File

@ -197,6 +197,7 @@ perDay: "Per Day"
stopActivityDelivery: "Stop sending activities" stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance" blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance" silenceThisInstance: "Silence this instance"
sensitiveMediaThisInstance: "Mark media from this instance as sensitive"
operations: "Operations" operations: "Operations"
software: "Software" software: "Software"
version: "Version" 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." 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" 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." 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" muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users" mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"

3
locales/index.d.ts vendored
View File

@ -204,6 +204,7 @@ export interface Locale {
"stopActivityDelivery": string; "stopActivityDelivery": string;
"blockThisInstance": string; "blockThisInstance": string;
"silenceThisInstance": string; "silenceThisInstance": string;
"sensitiveMediaThisInstance": string;
"operations": string; "operations": string;
"software": string; "software": string;
"version": string; "version": string;
@ -225,6 +226,8 @@ export interface Locale {
"blockedInstancesDescription": string; "blockedInstancesDescription": string;
"silencedInstances": string; "silencedInstances": string;
"silencedInstancesDescription": string; "silencedInstancesDescription": string;
"sensitiveMediaInstances": string;
"sensitiveMediaInstancesDescription": string;
"muteAndBlock": string; "muteAndBlock": string;
"mutedUsers": string; "mutedUsers": string;
"blockedUsers": string; "blockedUsers": string;

View File

@ -201,6 +201,7 @@ perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止" stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このサーバーをブロック" blockThisInstance: "このサーバーをブロック"
silenceThisInstance: "サーバーをサイレンス" silenceThisInstance: "サーバーをサイレンス"
sensitiveMediaThisInstance: "このサーバーのメディアを全てセンシティブとして設定"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -222,6 +223,8 @@ blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたサーバー" silencedInstances: "サイレンスしたサーバー"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
sensitiveMediaInstances: "センシティブなメディアを含むサーバー"
sensitiveMediaInstancesDescription: "センシティブなメディアを含むサーバーのホストを改行で区切って設定します。このサーバーからのメディアは全てセンシティブとして扱われます。"
muteAndBlock: "ミュートとブロック" muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー" mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー" blockedUsers: "ブロックしたユーザー"

View File

@ -201,6 +201,7 @@ perDay: "1일마다"
stopActivityDelivery: "액티비티 보내지 않기" stopActivityDelivery: "액티비티 보내지 않기"
blockThisInstance: "이 서버를 차단" blockThisInstance: "이 서버를 차단"
silenceThisInstance: "서버를 사일런스" silenceThisInstance: "서버를 사일런스"
sensitiveMediaThisInstance: "이 서버의 미디어를 모두 민감한 미디어로 표시"
operations: "작업" operations: "작업"
software: "소프트웨어" software: "소프트웨어"
version: "버전" version: "버전"
@ -222,6 +223,8 @@ blockedInstances: "차단된 서버"
blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다." blockedInstancesDescription: "차단하려는 서버의 호스트 이름을 줄바꿈으로 구분하여 설정합니다. 차단된 인스턴스는 이 인스턴스와 통신할 수 없게 됩니다."
silencedInstances: "사일런스한 서버" silencedInstances: "사일런스한 서버"
silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다." silencedInstancesDescription: "사일런스하려는 서버의 호스트명을 한 줄에 하나씩 입력합니다. 사일런스된 서버에 소속된 유저는 모두 '사일런스'된 상태로 취급되며, 이 서버로부터의 팔로우가 프로필 설정과 무관하게 승인제로 변경되고, 팔로워가 아닌 로컬 유저에게는 멘션할 수 없게 됩니다. 정지된 서버에는 적용되지 않습니다."
sensitiveMediaInstances: "민감한 미디어를 포함한 서버"
sensitiveMediaInstancesDescription: "민감한 미디어를 포함한 서버의 호스트명을 한 줄에 하나씩 입력합니다. 이 서버에 소속된 유저가 업로드한 미디어는 모두 민감한 미디어로 표시됩니다."
muteAndBlock: "뮤트 및 차단" muteAndBlock: "뮤트 및 차단"
mutedUsers: "뮤트한 유저" mutedUsers: "뮤트한 유저"
blockedUsers: "차단한 유저" blockedUsers: "차단한 유저"

View File

@ -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"`);
}
}

View File

@ -42,6 +42,12 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); 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 @bindThis
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
if (sensitiveWords.length === 0) return false; if (sensitiveWords.length === 0) return false;

View File

@ -173,6 +173,9 @@ export class ApNoteService {
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
const apHashtags = extractApHashtags(note.tag); 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は必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない // TODO: attachmentは必ずしも配列ではない
@ -180,7 +183,7 @@ export class ApNoteService {
const files = (await Promise.all(toArray(note.attachment).map(attach => ( const files = (await Promise.all(toArray(note.attachment).map(attach => (
limit(() => this.apImageService.resolveImage(actor, { limit(() => this.apImageService.resolveImage(actor, {
...attach, ...attach,
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする sensitive: isSensitiveMediaHost || note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
})) }))
)))); ))));

View File

@ -43,6 +43,7 @@ export class InstanceEntityService {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail, maintainerEmail: instance.maintainerEmail,
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
isSensitiveMedia: this.utilityService.isSensitiveMediaHost(meta.sensitiveMediaHosts, instance.host),
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl, faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor, themeColor: instance.themeColor,

View File

@ -81,6 +81,11 @@ export class MiMeta {
}) })
public silencedHosts: string[]; public silencedHosts: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public sensitiveMediaHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
nullable: true, nullable: true,

View File

@ -83,6 +83,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
isSensitiveMedia: {
type: 'boolean',
optional: false, nullable: false,
},
iconUrl: { iconUrl: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,

View File

@ -116,6 +116,16 @@ export const meta = {
nullable: false, nullable: false,
}, },
}, },
sensitiveMediaHosts: {
type: 'array',
optional: true,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: { pinnedUsers: {
type: 'array', type: 'array',
optional: false, nullable: false, optional: false, nullable: false,
@ -491,6 +501,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts, silencedHosts: instance.silencedHosts,
sensitiveMediaHosts: instance.sensitiveMediaHosts,
sensitiveWords: instance.sensitiveWords, sensitiveWords: instance.sensitiveWords,
preservedUsernames: instance.preservedUsernames, preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey, hcaptchaSecretKey: instance.hcaptchaSecretKey,

View File

@ -138,6 +138,11 @@ export const paramDef = {
type: 'string', type: 'string',
}, },
}, },
sensitiveMediaHosts: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
urlPreviewDenyList: { type: 'array', nullable: true, items: { urlPreviewDenyList: { type: 'array', nullable: true, items: {
type: 'string', type: 'string',
} }, } },
@ -173,13 +178,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.sensitiveWords)) { if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean); set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
} }
if (Array.isArray(ps.silencedHosts)) { if (Array.isArray(ps.silencedHosts)) {
let lastValue = ''; let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => { set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
const lv = lastValue; const lv = lastValue;
lastValue = h; lastValue = h;
return h !== '' && h !== lv && !set.blockedHosts?.includes(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)) { if (Array.isArray(ps.urlPreviewDenyList)) {

View File

@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span>{{ i18n.ts.silencedInstances }}</span> <span>{{ i18n.ts.silencedInstances }}</span>
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea> </MkTextarea>
<MkTextarea v-else-if="tab === 'sensitive'" v-model="sensitiveMediaHosts" class="_formBlock">
<span>{{ i18n.ts.sensitiveMediaInstances }}</span>
<template #caption>{{ i18n.ts.sensitiveMediaInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -35,18 +39,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const blockedHosts = ref<string>(''); const blockedHosts = ref<string>('');
const silencedHosts = ref<string>(''); const silencedHosts = ref<string>('');
const sensitiveMediaHosts = ref<string>('');
const tab = ref('block'); const tab = ref('block');
async function init() { async function init() {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
blockedHosts.value = meta.blockedHosts.join('\n'); blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts.join('\n'); silencedHosts.value = meta.silencedHosts.join('\n');
sensitiveMediaHosts.value = meta.sensitiveMediaHosts.join('\n');
} }
function save() { function save() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.value.split('\n') || [], blockedHosts: blockedHosts.value.split('\n') || [],
silencedHosts: silencedHosts.value.split('\n') || [], silencedHosts: silencedHosts.value.split('\n') || [],
sensitiveMediaHosts: sensitiveMediaHosts.value.split('\n') || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
@ -63,6 +70,10 @@ const headerTabs = computed(() => [{
key: 'silence', key: 'silence',
title: i18n.ts.silence, title: i18n.ts.silence,
icon: 'ti ti-eye-off', icon: 'ti ti-eye-off',
}, {
key: 'sensitive',
title: i18n.ts.sensitive,
icon: 'ti ti-photo-exclamation',
}]); }]);
definePageMetadata({ definePageMetadata({

View File

@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> <MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkSwitch v-model="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch>
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
</div> </div>
</FormSection> </FormSection>
@ -149,6 +150,7 @@ const instance = ref<Misskey.entities.FederationInstance | null>(null);
const suspended = ref(false); const suspended = ref(false);
const isBlocked = ref(false); const isBlocked = ref(false);
const isSilenced = ref(false); const isSilenced = ref(false);
const isSensitiveMedia = ref(false);
const faviconUrl = ref<string | null>(null); const faviconUrl = ref<string | null>(null);
const usersPagination = { const usersPagination = {
@ -172,6 +174,7 @@ async function fetch(): Promise<void> {
suspended.value = instance.value?.isSuspended ?? false; suspended.value = instance.value?.isSuspended ?? false;
isBlocked.value = instance.value?.isBlocked ?? false; isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? 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'); faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
} }
@ -194,6 +197,16 @@ async function toggleSilenced(): Promise<void> {
}); });
} }
async function toggleSensitiveMedia(): Promise<void> {
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<void> { async function toggleSuspend(): Promise<void> {
if (!instance.value) throw new Error('No instance?'); if (!instance.value) throw new Error('No instance?');
await os.api('admin/federation/update-instance', { await os.api('admin/federation/update-instance', {

View File

@ -2,8 +2,8 @@
/* eslint @typescript-eslint/no-explicit-any: 0 */ /* eslint @typescript-eslint/no-explicit-any: 0 */
/* /*
* version: 2023.12.2-io * version: 2023.11.1-io.3a
* generatedAt: 2023-12-28T08:11:12.906Z * generatedAt: 2024-01-07T10:20:39.681Z
*/ */
/** /**
@ -4209,6 +4209,7 @@ export type components = {
maintainerName: string | null; maintainerName: string | null;
maintainerEmail: string | null; maintainerEmail: string | null;
isSilenced: boolean; isSilenced: boolean;
isSensitiveMedia: boolean;
/** Format: url */ /** Format: url */
iconUrl: string | null; iconUrl: string | null;
/** Format: url */ /** Format: url */
@ -4560,6 +4561,7 @@ export type operations = {
enableServiceWorker: boolean; enableServiceWorker: boolean;
translatorAvailable: boolean; translatorAvailable: boolean;
silencedHosts?: string[]; silencedHosts?: string[];
sensitiveMediaHosts?: string[];
pinnedUsers: string[]; pinnedUsers: string[];
hiddenTags: string[]; hiddenTags: string[];
blockedHosts: string[]; blockedHosts: string[];
@ -8676,6 +8678,7 @@ export type operations = {
perUserListTimelineCacheMax?: number; perUserListTimelineCacheMax?: number;
notesPerOneAd?: number; notesPerOneAd?: number;
silencedHosts?: string[] | null; silencedHosts?: string[] | null;
sensitiveMediaHosts?: string[] | null;
urlPreviewDenyList?: string[] | null; urlPreviewDenyList?: string[] | null;
}; };
}; };
@ -25241,7 +25244,7 @@ export type operations = {
* @default other * @default other
* @enum {string} * @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';
}; };
}; };
}; };