diff options
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | locales/index.d.ts | 12 | ||||
| -rw-r--r-- | locales/ja-JP.yml | 3 | ||||
| -rw-r--r-- | packages/backend/migration/1716197366117-MediaSilenceForHosts.js | 16 | ||||
| -rw-r--r-- | packages/backend/src/core/DriveService.ts | 3 | ||||
| -rw-r--r-- | packages/backend/src/core/NoteCreateService.ts | 3 | ||||
| -rw-r--r-- | packages/backend/src/core/ReactionService.ts | 9 | ||||
| -rw-r--r-- | packages/backend/src/core/UtilityService.ts | 6 | ||||
| -rw-r--r-- | packages/backend/src/core/entities/InstanceEntityService.ts | 1 | ||||
| -rw-r--r-- | packages/backend/src/models/Meta.ts | 5 | ||||
| -rw-r--r-- | packages/backend/src/models/json-schema/federation-instance.ts | 4 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/admin/meta.ts | 11 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/admin/update-meta.ts | 15 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/instance-block.vue | 27 | ||||
| -rw-r--r-- | packages/frontend/src/pages/instance-info.vue | 15 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 3 |
16 files changed, 124 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e4b236eb..27308cb8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます - Feat: ユーザ作成時にSystemWebhookを送信可能に #14281 +- Feat: メディアサイレンスを実装 #13842 + - メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。 - Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 diff --git a/locales/index.d.ts b/locales/index.d.ts index aee0b6127e..f03207e0bd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -865,6 +865,10 @@ export interface Locale extends ILocale { */ "silenceThisInstance": string; /** + * サーバーをメディアサイレンス + */ + "mediaSilenceThisInstance": string; + /** * 操作 */ "operations": string; @@ -949,6 +953,14 @@ export interface Locale extends ILocale { */ "silencedInstancesDescription": string; /** + * メディアサイレンスしたサーバー + */ + "mediaSilencedInstances": string; + /** + * メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。 + */ + "mediaSilencedInstancesDescription": string; + /** * ミュートとブロック */ "muteAndBlock": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9b907f0971..d4931ae90d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -212,6 +212,7 @@ perDay: "1日ごと" stopActivityDelivery: "アクティビティの配送を停止" blockThisInstance: "このサーバーをブロック" silenceThisInstance: "サーバーをサイレンス" +mediaSilenceThisInstance: "サーバーをメディアサイレンス" operations: "操作" software: "ソフトウェア" version: "バージョン" @@ -233,6 +234,8 @@ blockedInstances: "ブロックしたサーバー" blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。" silencedInstances: "サイレンスしたサーバー" silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。" +mediaSilencedInstances: "メディアサイレンスしたサーバー" +mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。" muteAndBlock: "ミュートとブロック" mutedUsers: "ミュートしたユーザー" blockedUsers: "ブロックしたユーザー" diff --git a/packages/backend/migration/1716197366117-MediaSilenceForHosts.js b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js new file mode 100644 index 0000000000..10bb7f0255 --- /dev/null +++ b/packages/backend/migration/1716197366117-MediaSilenceForHosts.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MediaSilenceForHosts1716197366117 { + name = 'MediaSilenceForHosts1716197366117' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`); + } +} diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 37c5d1adf7..8aa04b4da7 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js'; import { correctFilename } from '@/misc/correct-filename.js'; import { isMimeImage } from '@/misc/is-mime-image.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { UtilityService } from '@/core/UtilityService.js'; type AddFileArgs = { /** User who wish to add file */ @@ -127,6 +128,7 @@ export class DriveService { private driveChart: DriveChart, private perUserDriveChart: PerUserDriveChart, private instanceChart: InstanceChart, + private utilityService: UtilityService, ) { const logger = new Logger('drive', 'blue'); this.registerLogger = logger.createSubLogger('register', 'yellow'); @@ -587,6 +589,7 @@ export class DriveService { sensitive ?? false : false; + if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index fd9fac357f..32cf3f3e26 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -364,6 +364,9 @@ export class NoteCreateService implements OnApplicationShutdown { mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens); } + // if the host is media-silenced, custom emojis are not allowed + if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 64c7b2ed03..371207c33a 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -105,6 +105,8 @@ export class ReactionService { @bindThis public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { + const meta = await this.metaService.fetch(); + // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -148,6 +150,11 @@ export class ReactionService { if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) { reaction = FALLBACK; } + + // for media silenced host, custom emoji reactions are not allowed + if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { + reaction = FALLBACK; + } } else { // リアクションとして使う権限がない reaction = FALLBACK; @@ -220,8 +227,6 @@ export class ReactionService { } } - const meta = await this.metaService.fetch(); - if (meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 652e8f7449..94729250a6 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -43,6 +43,12 @@ export class UtilityService { } @bindThis + public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean { + if (!silencedHosts || host == null) return false; + return silencedHosts.some(x => host.toLowerCase() === x); + } + + @bindThis public concatNoteContentsForKeyWordCheck(content: { cw?: string | null; text?: string | null; diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 9117b13914..4c45c13167 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -50,6 +50,7 @@ export class InstanceEntityService { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), + isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, 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 ad306fcad6..70d41801b5 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -87,6 +87,11 @@ export class MiMeta { public silencedHosts: string[]; @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public mediaSilencedHosts: 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 ed40d405c6..912a0399d8 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isMediaSilenced: { + 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 eee02a7123..2e7f73da73 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -128,6 +128,16 @@ export const meta = { nullable: false, }, }, + mediaSilencedHosts: { + type: 'array', + optional: false, + nullable: false, + items: { + type: 'string', + optional: false, + nullable: false, + }, + }, pinnedUsers: { type: 'array', optional: false, nullable: false, @@ -552,6 +562,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, silencedHosts: instance.silencedHosts, + mediaSilencedHosts: instance.mediaSilencedHosts, sensitiveWords: instance.sensitiveWords, prohibitedWords: instance.prohibitedWords, preservedUsernames: instance.preservedUsernames, 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 4e28ee6877..5efdc9d8c4 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -150,6 +150,13 @@ export const paramDef = { type: 'string', }, }, + mediaSilencedHosts: { + type: 'array', + nullable: true, + items: { + type: 'string', + }, + }, summalyProxy: { type: 'string', nullable: true, description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', @@ -203,6 +210,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- return h !== '' && h !== lv && !set.blockedHosts?.includes(h); }); } + if (Array.isArray(ps.mediaSilencedHosts)) { + let lastValue = ''; + set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => { + const lv = lastValue; + lastValue = h; + return h !== '' && h !== lv && !set.blockedHosts?.includes(h); + }); + } if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index 6b14bd42c2..e090616b26 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -8,14 +8,22 @@ SPDX-License-Identifier: AGPL-3.0-only <template #header><XHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="700" :marginMin="16" :marginMax="32"> <FormSuspense :p="init"> - <MkTextarea v-if="tab === 'block'" v-model="blockedHosts"> - <span>{{ i18n.ts.blockedInstances }}</span> - <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> - </MkTextarea> - <MkTextarea v-else-if="tab === 'silence'" v-model="silencedHosts" class="_formBlock"> - <span>{{ i18n.ts.silencedInstances }}</span> - <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> - </MkTextarea> + <template v-if="tab === 'block'"> + <MkTextarea v-model="blockedHosts"> + <span>{{ i18n.ts.blockedInstances }}</span> + <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> + </MkTextarea> + </template> + <template v-else-if="tab === 'silence'"> + <MkTextarea v-model="silencedHosts" class="_formBlock"> + <span>{{ i18n.ts.silencedInstances }}</span> + <template #caption>{{ i18n.ts.silencedInstancesDescription }}</template> + </MkTextarea> + <MkTextarea v-model="mediaSilencedHosts" class="_formBlock"> + <span>{{ i18n.ts.mediaSilencedInstances }}</span> + <template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template> + </MkTextarea> + </template> <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> </FormSuspense> </MkSpacer> @@ -36,18 +44,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const blockedHosts = ref<string>(''); const silencedHosts = ref<string>(''); +const mediaSilencedHosts = ref<string>(''); const tab = ref('block'); async function init() { const meta = await misskeyApi('admin/meta'); blockedHosts.value = meta.blockedHosts.join('\n'); silencedHosts.value = meta.silencedHosts.join('\n'); + mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); } function save() { os.apiWithDialog('admin/update-meta', { blockedHosts: blockedHosts.value.split('\n') || [], silencedHosts: silencedHosts.value.split('\n') || [], + mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], }).then(() => { fetchInstance(true); diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 26797ba85e..4ba428d536 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -47,6 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="suspensionState !== 'none'" :disabled="!instance" @click="resumeDelivery">{{ i18n.ts._delivery.resume }}</MkButton> <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="isMediaSilenced" :disabled="!meta || !instance" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkTextarea v-model="moderationNote" manualSave> <template #label>{{ i18n.ts.moderationNote }}</template> @@ -167,6 +168,7 @@ const instance = ref<Misskey.entities.FederationInstance | null>(null); const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding'>('none'); const isBlocked = ref(false); const isSilenced = ref(false); +const isMediaSilenced = ref(false); const faviconUrl = ref<string | null>(null); const moderationNote = ref(''); @@ -195,8 +197,9 @@ async function fetch(): Promise<void> { suspensionState.value = instance.value?.suspensionState ?? 'none'; isBlocked.value = instance.value?.isBlocked ?? false; isSilenced.value = instance.value?.isSilenced ?? false; + isMediaSilenced.value = instance.value?.isMediaSilenced ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); - moderationNote.value = instance.value?.moderationNote; + moderationNote.value = instance.value?.moderationNote ?? ''; } async function toggleBlock(): Promise<void> { @@ -218,6 +221,16 @@ async function toggleSilenced(): Promise<void> { }); } +async function toggleMediaSilenced(): Promise<void> { + if (!meta.value) throw new Error('No meta?'); + if (!instance.value) throw new Error('No instance?'); + const { host } = instance.value; + const mediaSilencedHosts = meta.value.mediaSilencedHosts ?? []; + await misskeyApi('admin/update-meta', { + mediaSilencedHosts: isMediaSilenced.value ? mediaSilencedHosts.concat([host]) : mediaSilencedHosts.filter(x => x !== host), + }); +} + async function stopDelivery(): Promise<void> { if (!instance.value) throw new Error('No instance?'); suspensionState.value = 'manuallySuspended'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index cb7d82d2d8..db5efd4a00 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4599,6 +4599,7 @@ export type components = { maintainerName: string | null; maintainerEmail: string | null; isSilenced: boolean; + isMediaSilenced: boolean; /** Format: url */ iconUrl: string | null; /** Format: url */ @@ -5044,6 +5045,7 @@ export type operations = { enableServiceWorker: boolean; translatorAvailable: boolean; silencedHosts?: string[]; + mediaSilencedHosts: string[]; pinnedUsers: string[]; hiddenTags: string[]; blockedHosts: string[]; @@ -9371,6 +9373,7 @@ export type operations = { perUserListTimelineCacheMax?: number; notesPerOneAd?: number; silencedHosts?: string[] | null; + mediaSilencedHosts?: string[] | null; /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */ summalyProxy?: string | null; urlPreviewEnabled?: boolean; |