merge: Add option to reject reports from an instance (Resolves #579, #715, #716) (!662)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/662

Closes #579, #715, and #716

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Marie 2024-10-06 19:39:38 +00:00
commit 28bfd87537
10 changed files with 125 additions and 2 deletions

View file

@ -154,6 +154,7 @@ renoteUnmute: "Unmute Boosts"
block: "Block" block: "Block"
unblock: "Unblock" unblock: "Unblock"
markAsNSFW: "Mark all media from user as NSFW" markAsNSFW: "Mark all media from user as NSFW"
markInstanceAsNSFW: "Mark as NSFW"
suspend: "Suspend" suspend: "Suspend"
unsuspend: "Unsuspend" unsuspend: "Unsuspend"
blockConfirm: "Are you sure that you want to block this account?" blockConfirm: "Are you sure that you want to block this account?"
@ -228,6 +229,7 @@ stopActivityDelivery: "Stop sending activities"
blockThisInstance: "Block this instance" blockThisInstance: "Block this instance"
silenceThisInstance: "Silence this instance" silenceThisInstance: "Silence this instance"
mediaSilenceThisInstance: "Silence media from this instance" mediaSilenceThisInstance: "Silence media from this instance"
rejectReports: "Reject reports from this instance"
operations: "Operations" operations: "Operations"
software: "Software" software: "Software"
version: "Version" version: "Version"
@ -2579,6 +2581,10 @@ _moderationLogTypes:
resetPassword: "Password reset" resetPassword: "Password reset"
suspendRemoteInstance: "Remote instance suspended" suspendRemoteInstance: "Remote instance suspended"
unsuspendRemoteInstance: "Remote instance unsuspended" unsuspendRemoteInstance: "Remote instance unsuspended"
setRemoteInstanceNSFW: "Set remote instance as NSFW"
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
rejectRemoteInstanceReports: "Rejected reports from remote instance"
acceptRemoteInstanceReports: "Accepted reports from remote instance"
updateRemoteInstanceNote: "Moderation note updated for remote instance." updateRemoteInstanceNote: "Moderation note updated for remote instance."
markSensitiveDriveFile: "File marked as sensitive" markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive" unmarkSensitiveDriveFile: "File unmarked as sensitive"

24
locales/index.d.ts vendored
View file

@ -632,6 +632,10 @@ export interface Locale extends ILocale {
* NSFWとしてマークする * NSFWとしてマークする
*/ */
"markAsNSFW": string; "markAsNSFW": string;
/**
* Mark as NSFW
*/
"markInstanceAsNSFW": string;
/** /**
* *
*/ */
@ -928,6 +932,10 @@ export interface Locale extends ILocale {
* *
*/ */
"mediaSilenceThisInstance": string; "mediaSilenceThisInstance": string;
/**
* Reject reports from this instance
*/
"rejectReports": string;
/** /**
* *
*/ */
@ -10000,6 +10008,22 @@ export interface Locale extends ILocale {
* *
*/ */
"unsuspendRemoteInstance": string; "unsuspendRemoteInstance": string;
/**
* Set remote instance as NSFW
*/
"setRemoteInstanceNSFW": string;
/**
* Set remote instance as NSFW
*/
"unsetRemoteInstanceNSFW": string;
/**
* Rejected reports from remote instance
*/
"rejectRemoteInstanceReports": string;
/**
* Accepted reports from remote instance
*/
"acceptRemoteInstanceReports": string;
/** /**
* *
*/ */

View file

@ -154,6 +154,7 @@ renoteUnmute: "ブーストのミュートを解除"
block: "ブロック" block: "ブロック"
unblock: "ブロック解除" unblock: "ブロック解除"
markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする" markAsNSFW: "ユーザーのすべてのメディアをNSFWとしてマークする"
markInstanceAsNSFW: "Mark as NSFW"
suspend: "凍結" suspend: "凍結"
unsuspend: "解凍" unsuspend: "解凍"
blockConfirm: "ブロックしますか?" blockConfirm: "ブロックしますか?"
@ -228,6 +229,7 @@ stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このサーバーをブロック" blockThisInstance: "このサーバーをブロック"
silenceThisInstance: "サーバーをサイレンス" silenceThisInstance: "サーバーをサイレンス"
mediaSilenceThisInstance: "サーバーをメディアサイレンス" mediaSilenceThisInstance: "サーバーをメディアサイレンス"
rejectReports: "Reject reports from this instance"
operations: "操作" operations: "操作"
software: "ソフトウェア" software: "ソフトウェア"
version: "バージョン" version: "バージョン"
@ -2647,6 +2649,10 @@ _moderationLogTypes:
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを停止" suspendRemoteInstance: "リモートサーバーを停止"
unsuspendRemoteInstance: "リモートサーバーを再開" unsuspendRemoteInstance: "リモートサーバーを再開"
setRemoteInstanceNSFW: "Set remote instance as NSFW"
unsetRemoteInstanceNSFW: "Set remote instance as NSFW"
rejectRemoteInstanceReports: "Rejected reports from remote instance"
acceptRemoteInstanceReports: "Accepted reports from remote instance"
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新" updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddRejectReports1728177700920 {
name = 'AddRejectReports1728177700920'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "rejectReports" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "rejectReports"`);
}
}

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import * as Bull from 'bullmq';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { UserFollowingService } from '@/core/UserFollowingService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js';
@ -29,6 +30,7 @@ import { bindThis } from '@/decorators.js';
import type { MiRemoteUser } from '@/models/User.js'; import type { MiRemoteUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AbuseReportService } from '@/core/AbuseReportService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
@ -83,6 +85,7 @@ export class ApInboxService {
private apQuestionService: ApQuestionService, private apQuestionService: ApQuestionService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private federatedInstanceService: FederatedInstanceService,
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
} }
@ -530,6 +533,12 @@ export class ApInboxService {
@bindThis @bindThis
private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> { private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> {
// Make sure the source instance is allowed to send reports.
const instance = await this.federatedInstanceService.fetch(actor.host);
if (instance.rejectReports) {
throw new Bull.UnrecoverableError(`Rejecting report from instance: ${actor.host}`);
}
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object); const uris = getApIds(activity.object);

View file

@ -158,7 +158,12 @@ export class MiInstance {
default: false, default: false,
}) })
public isNSFW: boolean; public isNSFW: boolean;
@Column('boolean', {
default: false,
})
public rejectReports: boolean;
@Column('varchar', { @Column('varchar', {
length: 16384, default: '', length: 16384, default: '',
}) })

View file

@ -25,6 +25,7 @@ export const paramDef = {
host: { type: 'string' }, host: { type: 'string' },
isSuspended: { type: 'boolean' }, isSuspended: { type: 'boolean' },
isNSFW: { type: 'boolean' }, isNSFW: { type: 'boolean' },
rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' }, moderationNote: { type: 'string' },
}, },
required: ['host'], required: ['host'],
@ -57,6 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.federatedInstanceService.update(instance.id, { await this.federatedInstanceService.update(instance.id, {
suspensionState, suspensionState,
isNSFW: ps.isNSFW, isNSFW: ps.isNSFW,
rejectReports: ps.rejectReports,
moderationNote: ps.moderationNote, moderationNote: ps.moderationNote,
}); });
@ -74,6 +76,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
if (ps.isNSFW != null && instance.isNSFW !== ps.isNSFW) {
const message = ps.rejectReports ? 'setRemoteInstanceNSFW' : 'unsetRemoteInstanceNSFW';
this.moderationLogService.log(me, message, {
id: instance.id,
host: instance.host,
});
}
if (ps.rejectReports != null && instance.rejectReports !== ps.rejectReports) {
const message = ps.rejectReports ? 'rejectRemoteInstanceReports' : 'acceptRemoteInstanceReports';
this.moderationLogService.log(me, message, {
id: instance.id,
host: instance.host,
});
}
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) { if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', { this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id, id: instance.id,

View file

@ -77,8 +77,12 @@ export const moderationLogTypes = [
'deleteGlobalAnnouncement', 'deleteGlobalAnnouncement',
'deleteUserAnnouncement', 'deleteUserAnnouncement',
'resetPassword', 'resetPassword',
'setRemoteInstanceNSFW',
'unsetRemoteInstanceNSFW',
'suspendRemoteInstance', 'suspendRemoteInstance',
'unsuspendRemoteInstance', 'unsuspendRemoteInstance',
'rejectRemoteInstanceReports',
'acceptRemoteInstanceReports',
'updateRemoteInstanceNote', 'updateRemoteInstanceNote',
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
@ -227,6 +231,14 @@ export type ModerationLogPayloads = {
userUsername: string; userUsername: string;
userHost: string | null; userHost: string | null;
}; };
setRemoteInstanceNSFW: {
id: string;
host: string;
};
unsetRemoteInstanceNSFW: {
id: string;
host: string;
};
suspendRemoteInstance: { suspendRemoteInstance: {
id: string; id: string;
host: string; host: string;
@ -235,6 +247,14 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
rejectRemoteInstanceReports: {
id: string;
host: string;
};
acceptRemoteInstanceReports: {
id: string;
host: string;
};
updateRemoteInstanceNote: { updateRemoteInstanceNote: {
id: string; id: string;
host: string; host: string;

View file

@ -23,6 +23,10 @@ SPDX-License-Identifier: AGPL-3.0-only
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'resetPassword', 'resetPassword',
'suspendRemoteInstance', 'suspendRemoteInstance',
'setRemoteInstanceNSFW',
'unsetRemoteInstanceNSFW',
'rejectRemoteInstanceReports',
'acceptRemoteInstanceReports',
].includes(log.type), ].includes(log.type),
[$style.logRed]: [ [$style.logRed]: [
'suspend', 'suspend',
@ -61,6 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span> <span v-else-if="log.type === 'unmarkSensitiveDriveFile'">: @{{ log.info.fileUserUsername }}{{ log.info.fileUserHost ? '@' + log.info.fileUserHost : '' }}</span>
<span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span> <span v-else-if="log.type === 'suspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span> <span v-else-if="log.type === 'unsuspendRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'setRemoteInstanceNSFW'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unsetRemoteInstanceNSFW'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'rejectRemoteInstanceReports'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'acceptRemoteInstanceReports'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> <span v-else-if="log.type === 'createGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>
<span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span> <span v-else-if="log.type === 'updateGlobalAnnouncement'">: {{ log.info.before.title }}</span>
<span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span> <span v-else-if="log.type === 'deleteGlobalAnnouncement'">: {{ log.info.announcement.title }}</span>

View file

@ -49,7 +49,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> <MkSwitch v-model="isBlocked" :disabled="!meta || !instance || isBaseBlocked" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo> <MkInfo v-if="isBaseSilenced" warn>{{ i18n.ts.silencedByBase }}</MkInfo>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> <MkSwitch v-model="isSilenced" :disabled="!meta || !instance || isBaseSilenced" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
<MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">Mark as NSFW</MkSwitch> <MkSwitch v-model="isNSFW" :disabled="!instance" @update:modelValue="toggleNSFW">{{ i18n.ts.markInstanceAsNSFW }}</MkSwitch>
<MkSwitch v-model="rejectReports" :disabled="!instance" @update:modelValue="toggleRejectReports">{{ i18n.ts.rejectReports }}</MkSwitch>
<MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo> <MkInfo v-if="isBaseMediaSilenced" warn>{{ i18n.ts.mediaSilencedByBase }}</MkInfo>
<MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</MkSwitch> <MkSwitch v-model="isMediaSilenced" :disabled="!meta || !instance || isBaseMediaSilenced" @update:modelValue="toggleMediaSilenced">{{ i18n.ts.mediaSilenceThisInstance }}</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>
@ -174,6 +175,7 @@ const suspensionState = ref<'none' | 'manuallySuspended' | 'goneSuspended' | 'au
const isBlocked = ref(false); const isBlocked = ref(false);
const isSilenced = ref(false); const isSilenced = ref(false);
const isNSFW = ref(false); const isNSFW = ref(false);
const rejectReports = ref(false);
const isMediaSilenced = ref(false); const isMediaSilenced = ref(false);
const faviconUrl = ref<string | null>(null); const faviconUrl = ref<string | null>(null);
const moderationNote = ref(''); const moderationNote = ref('');
@ -219,6 +221,7 @@ async function fetch(): Promise<void> {
isBlocked.value = instance.value?.isBlocked ?? false; isBlocked.value = instance.value?.isBlocked ?? false;
isSilenced.value = instance.value?.isSilenced ?? false; isSilenced.value = instance.value?.isSilenced ?? false;
isNSFW.value = instance.value?.isNSFW ?? false; isNSFW.value = instance.value?.isNSFW ?? false;
rejectReports.value = instance.value?.rejectReports ?? false;
isMediaSilenced.value = instance.value?.isMediaSilenced ?? false; isMediaSilenced.value = instance.value?.isMediaSilenced ?? 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');
moderationNote.value = instance.value?.moderationNote ?? ''; moderationNote.value = instance.value?.moderationNote ?? '';
@ -279,6 +282,14 @@ async function toggleNSFW(): Promise<void> {
}); });
} }
async function toggleRejectReports(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
rejectReports: rejectReports.value,
});
}
function refreshMetadata(): void { function refreshMetadata(): void {
if (!instance.value) throw new Error('No instance?'); if (!instance.value) throw new Error('No instance?');
misskeyApi('admin/federation/refresh-remote-instance-metadata', { misskeyApi('admin/federation/refresh-remote-instance-metadata', {