feat(federation): 特定の連合サーバーのメディアを全てセンシティブとして設定する機能を追加 (MisskeyIO#340)
This commit is contained in:
@@ -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"`);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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にする
|
||||
}))
|
||||
))));
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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<typeof meta, typeof paramDef> { // eslint-
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
silencedHosts: instance.silencedHosts,
|
||||
sensitiveMediaHosts: instance.sensitiveMediaHosts,
|
||||
sensitiveWords: instance.sensitiveWords,
|
||||
preservedUsernames: instance.preservedUsernames,
|
||||
hcaptchaSecretKey: instance.hcaptchaSecretKey,
|
||||
|
@@ -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<typeof meta, typeof paramDef> { // 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)) {
|
||||
|
@@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span>{{ i18n.ts.silencedInstances }}</span>
|
||||
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
|
||||
</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>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@@ -35,18 +39,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
const blockedHosts = ref<string>('');
|
||||
const silencedHosts = ref<string>('');
|
||||
const sensitiveMediaHosts = ref<string>('');
|
||||
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({
|
||||
|
@@ -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="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="isSensitiveMedia" :disabled="!meta || !instance" @update:modelValue="toggleSensitiveMedia">{{ i18n.ts.sensitiveMediaThisInstance }}</MkSwitch>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
</div>
|
||||
</FormSection>
|
||||
@@ -149,6 +150,7 @@ const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||
const suspended = ref(false);
|
||||
const isBlocked = ref(false);
|
||||
const isSilenced = ref(false);
|
||||
const isSensitiveMedia = ref(false);
|
||||
const faviconUrl = ref<string | null>(null);
|
||||
|
||||
const usersPagination = {
|
||||
@@ -172,6 +174,7 @@ async function fetch(): Promise<void> {
|
||||
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<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> {
|
||||
if (!instance.value) throw new Error('No instance?');
|
||||
await os.api('admin/federation/update-instance', {
|
||||
|
@@ -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';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user