Skip to content

Commit 5c42a0e

Browse files
feat: media silence (#13842)
* feat: media silence * fix: lint * feat: deny creating custom emoji reaction and using custom emoji from media silenced hosts * chore: メディアサイレンスの説明にカスタム絵文字の話を追加 * Update locales/ja-JP.yml Co-authored-by: Sayamame-beans <[email protected]> * chore: update index.d.ts * docs(changelog): update changelog --------- Co-authored-by: Sayamame-beans <[email protected]>
1 parent 8f40f93 commit 5c42a0e

File tree

16 files changed

+124
-11
lines changed

16 files changed

+124
-11
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
1010
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
1111
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
12+
- Feat: メディアサイレンスを実装 #13842
13+
- メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。
1214
- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
1315
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
1416
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題

locales/index.d.ts

+12
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,10 @@ export interface Locale extends ILocale {
864864
* サーバーをサイレンス
865865
*/
866866
"silenceThisInstance": string;
867+
/**
868+
* サーバーをメディアサイレンス
869+
*/
870+
"mediaSilenceThisInstance": string;
867871
/**
868872
* 操作
869873
*/
@@ -948,6 +952,14 @@ export interface Locale extends ILocale {
948952
* サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。
949953
*/
950954
"silencedInstancesDescription": string;
955+
/**
956+
* メディアサイレンスしたサーバー
957+
*/
958+
"mediaSilencedInstances": string;
959+
/**
960+
* メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
961+
*/
962+
"mediaSilencedInstancesDescription": string;
951963
/**
952964
* ミュートとブロック
953965
*/

locales/ja-JP.yml

+3
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ perDay: "1日ごと"
212212
stopActivityDelivery: "アクティビティの配送を停止"
213213
blockThisInstance: "このサーバーをブロック"
214214
silenceThisInstance: "サーバーをサイレンス"
215+
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
215216
operations: "操作"
216217
software: "ソフトウェア"
217218
version: "バージョン"
@@ -233,6 +234,8 @@ blockedInstances: "ブロックしたサーバー"
233234
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
234235
silencedInstances: "サイレンスしたサーバー"
235236
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
237+
mediaSilencedInstances: "メディアサイレンスしたサーバー"
238+
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
236239
muteAndBlock: "ミュートとブロック"
237240
mutedUsers: "ミュートしたユーザー"
238241
blockedUsers: "ブロックしたユーザー"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and misskey-project
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
export class MediaSilenceForHosts1716197366117 {
7+
name = 'MediaSilenceForHosts1716197366117'
8+
9+
async up(queryRunner) {
10+
await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
11+
}
12+
13+
async down(queryRunner) {
14+
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`);
15+
}
16+
}

packages/backend/src/core/DriveService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
4343
import { correctFilename } from '@/misc/correct-filename.js';
4444
import { isMimeImage } from '@/misc/is-mime-image.js';
4545
import { ModerationLogService } from '@/core/ModerationLogService.js';
46+
import { UtilityService } from '@/core/UtilityService.js';
4647

4748
type AddFileArgs = {
4849
/** User who wish to add file */
@@ -127,6 +128,7 @@ export class DriveService {
127128
private driveChart: DriveChart,
128129
private perUserDriveChart: PerUserDriveChart,
129130
private instanceChart: InstanceChart,
131+
private utilityService: UtilityService,
130132
) {
131133
const logger = new Logger('drive', 'blue');
132134
this.registerLogger = logger.createSubLogger('register', 'yellow');
@@ -587,6 +589,7 @@ export class DriveService {
587589
sensitive ?? false
588590
: false;
589591

592+
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
590593
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
591594
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
592595
if (userRoleNSFW) file.isSensitive = true;

packages/backend/src/core/NoteCreateService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ export class NoteCreateService implements OnApplicationShutdown {
364364
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
365365
}
366366

367+
// if the host is media-silenced, custom emojis are not allowed
368+
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
369+
367370
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
368371

369372
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {

packages/backend/src/core/ReactionService.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export class ReactionService {
105105

106106
@bindThis
107107
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
108+
const meta = await this.metaService.fetch();
109+
108110
// Check blocking
109111
if (note.userId !== user.id) {
110112
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@@ -148,6 +150,11 @@ export class ReactionService {
148150
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
149151
reaction = FALLBACK;
150152
}
153+
154+
// for media silenced host, custom emoji reactions are not allowed
155+
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
156+
reaction = FALLBACK;
157+
}
151158
} else {
152159
// リアクションとして使う権限がない
153160
reaction = FALLBACK;
@@ -220,8 +227,6 @@ export class ReactionService {
220227
}
221228
}
222229

223-
const meta = await this.metaService.fetch();
224-
225230
if (meta.enableChartsForRemoteUser || (user.host == null)) {
226231
this.perUserReactionsChart.update(user, note);
227232
}

packages/backend/src/core/UtilityService.ts

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export class UtilityService {
4242
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
4343
}
4444

45+
@bindThis
46+
public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
47+
if (!silencedHosts || host == null) return false;
48+
return silencedHosts.some(x => host.toLowerCase() === x);
49+
}
50+
4551
@bindThis
4652
public concatNoteContentsForKeyWordCheck(content: {
4753
cw?: string | null;

packages/backend/src/core/entities/InstanceEntityService.ts

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class InstanceEntityService {
5050
maintainerName: instance.maintainerName,
5151
maintainerEmail: instance.maintainerEmail,
5252
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
53+
isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
5354
iconUrl: instance.iconUrl,
5455
faviconUrl: instance.faviconUrl,
5556
themeColor: instance.themeColor,

packages/backend/src/models/Meta.ts

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ export class MiMeta {
8686
})
8787
public silencedHosts: string[];
8888

89+
@Column('varchar', {
90+
length: 1024, array: true, default: '{}',
91+
})
92+
public mediaSilencedHosts: string[];
93+
8994
@Column('varchar', {
9095
length: 1024,
9196
nullable: true,

packages/backend/src/models/json-schema/federation-instance.ts

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = {
8888
type: 'boolean',
8989
optional: false, nullable: false,
9090
},
91+
isMediaSilenced: {
92+
type: 'boolean',
93+
optional: false, nullable: false,
94+
},
9195
iconUrl: {
9296
type: 'string',
9397
optional: false, nullable: true,

packages/backend/src/server/api/endpoints/admin/meta.ts

+11
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ export const meta = {
128128
nullable: false,
129129
},
130130
},
131+
mediaSilencedHosts: {
132+
type: 'array',
133+
optional: false,
134+
nullable: false,
135+
items: {
136+
type: 'string',
137+
optional: false,
138+
nullable: false,
139+
},
140+
},
131141
pinnedUsers: {
132142
type: 'array',
133143
optional: false, nullable: false,
@@ -552,6 +562,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
552562
hiddenTags: instance.hiddenTags,
553563
blockedHosts: instance.blockedHosts,
554564
silencedHosts: instance.silencedHosts,
565+
mediaSilencedHosts: instance.mediaSilencedHosts,
555566
sensitiveWords: instance.sensitiveWords,
556567
prohibitedWords: instance.prohibitedWords,
557568
preservedUsernames: instance.preservedUsernames,

packages/backend/src/server/api/endpoints/admin/update-meta.ts

+15
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,13 @@ export const paramDef = {
150150
type: 'string',
151151
},
152152
},
153+
mediaSilencedHosts: {
154+
type: 'array',
155+
nullable: true,
156+
items: {
157+
type: 'string',
158+
},
159+
},
153160
summalyProxy: {
154161
type: 'string', nullable: true,
155162
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@@ -203,6 +210,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
203210
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
204211
});
205212
}
213+
if (Array.isArray(ps.mediaSilencedHosts)) {
214+
let lastValue = '';
215+
set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
216+
const lv = lastValue;
217+
lastValue = h;
218+
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
219+
});
220+
}
206221
if (ps.themeColor !== undefined) {
207222
set.themeColor = ps.themeColor;
208223
}

packages/frontend/src/pages/admin/instance-block.vue

+19-8
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ SPDX-License-Identifier: AGPL-3.0-only
88
<template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
99
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
1010
<FormSuspense :p="init">
11-
<MkTextarea v-if="tab === 'block'" v-model="blockedHosts">
12-
<span>{{ i18n.ts.blockedInstances }}</span>
13-
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
14-
</MkTextarea>
15-
<MkTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock">
16-
<span>{{ i18n.ts.silencedInstances }}</span>
17-
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
18-
</MkTextarea>
11+
<template v-if="tab === 'block'">
12+
<MkTextarea v-model="blockedHosts">
13+
<span>{{ i18n.ts.blockedInstances }}</span>
14+
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
15+
</MkTextarea>
16+
</template>
17+
<template v-else-if="tab === 'silence'">
18+
<MkTextarea v-model="silencedHosts" class="_formBlock">
19+
<span>{{ i18n.ts.silencedInstances }}</span>
20+
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
21+
</MkTextarea>
22+
<MkTextarea v-model="mediaSilencedHosts" class="_formBlock">
23+
<span>{{ i18n.ts.mediaSilencedInstances }}</span>
24+
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
25+
</MkTextarea>
26+
</template>
1927
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
2028
</FormSuspense>
2129
</MkSpacer>
@@ -36,18 +44,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
3644

3745
const blockedHosts = ref<string>('');
3846
const silencedHosts = ref<string>('');
47+
const mediaSilencedHosts = ref<string>('');
3948
const tab = ref('block');
4049

4150
async function init() {
4251
const meta = await misskeyApi('admin/meta');
4352
blockedHosts.value = meta.blockedHosts.join('\n');
4453
silencedHosts.value = meta.silencedHosts.join('\n');
54+
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
4555
}
4656

4757
function save() {
4858
os.apiWithDialog('admin/update-meta', {
4959
blockedHosts: blockedHosts.value.split('\n') || [],
5060
silencedHosts: silencedHosts.value.split('\n') || [],
61+
mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [],
5162

5263
}).then(() => {
5364
fetchInstance(true);

packages/frontend/src/pages/instance-info.vue

+14-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
4747
<MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton>
4848
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
4949
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
50+
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch>
5051
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
5152
<MkTextarea v-model="moderationNote" manualSave>
5253
<template #label>{{ i18n.ts.moderationNote }}</template>
@@ -167,6 +168,7 @@ const instance = ref<Misskey.entities.FederationInstance | null>(null);
167168
const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none');
168169
const isBlocked = ref(false);
169170
const isSilenced = ref(false);
171+
const isMediaSilenced = ref(false);
170172
const faviconUrl = ref<string | null>(null);
171173
const moderationNote = ref('');
172174

@@ -195,8 +197,9 @@ async function fetch(): Promise<void> {
195197
suspensionState.value = instance.value?.suspensionState ?? 'none';
196198
isBlocked.value = instance.value?.isBlocked ?? false;
197199
isSilenced.value = instance.value?.isSilenced ?? false;
200+
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false;
198201
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
199-
moderationNote.value = instance.value?.moderationNote;
202+
moderationNote.value = instance.value?.moderationNote ?? '';
200203
}
201204

202205
async function toggleBlock(): Promise<void> {
@@ -218,6 +221,16 @@ async function toggleSilenced(): Promise<void> {
218221
});
219222
}
220223

224+
async function toggleMediaSilenced(): Promise<void> {
225+
if (!meta.value) throw new Error('No meta?');
226+
if (!instance.value) throw new Error('No instance?');
227+
const { host } = instance.value;
228+
const mediaSilencedHosts = meta.value.mediaSilencedHosts ?? [];
229+
await misskeyApi('admin/update-meta', {
230+
mediaSilencedHosts: isMediaSilenced.value ? mediaSilencedHosts.concat([host]) : mediaSilencedHosts.filter(x => x !== host),
231+
});
232+
}
233+
221234
async function stopDelivery(): Promise<void> {
222235
if (!instance.value) throw new Error('No instance?');
223236
suspensionState.value = 'manuallySuspended';

packages/misskey-js/src/autogen/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4599,6 +4599,7 @@ export type components = {
45994599
maintainerName: string | null;
46004600
maintainerEmail: string | null;
46014601
isSilenced: boolean;
4602+
isMediaSilenced: boolean;
46024603
/** Format: url */
46034604
iconUrl: string | null;
46044605
/** Format: url */
@@ -5044,6 +5045,7 @@ export type operations = {
50445045
enableServiceWorker: boolean;
50455046
translatorAvailable: boolean;
50465047
silencedHosts?: string[];
5048+
mediaSilencedHosts: string[];
50475049
pinnedUsers: string[];
50485050
hiddenTags: string[];
50495051
blockedHosts: string[];
@@ -9371,6 +9373,7 @@ export type operations = {
93719373
perUserListTimelineCacheMax?: number;
93729374
notesPerOneAd?: number;
93739375
silencedHosts?: string[] | null;
9376+
mediaSilencedHosts?: string[] | null;
93749377
/** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
93759378
summalyProxy?: string | null;
93769379
urlPreviewEnabled?: boolean;

0 commit comments

Comments
 (0)