Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

公開投稿以外の配送を制限する機能 #574

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このサーバーをブロック"
silenceThisInstance: "サーバーをサイレンス"
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
quarantineThisInstance: "サーバーに公開投稿のみ配送"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
Expand Down Expand Up @@ -3088,6 +3089,8 @@ _moderationLogTypes:
deleteGalleryPost: "ギャラリーの投稿を削除"
updateOfficialTags: "公式タグ一覧を更新"
promoteQueue: "ジョブキューを再試行"
quarantineRemoteInstance: "公開投稿のみ配送に制限"
unquarantineRemoteInstance: "公開投稿のみ配送を解除"

_fileViewer:
title: "ファイルの詳細"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1734500881453-AddQuarantineLimited.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project, yojo-art team
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class AddQuarantineLimited1734500881453 {
name = 'AddQuarantineLimited1734500881453'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "quarantineLimited" boolean NOT NULL DEFAULT false`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "quarantineLimited"`);
}
}
1 change: 1 addition & 0 deletions packages/backend/src/core/GlobalEventService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export interface InternalEventTypes {
unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; };
userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; };
userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; };
clearQuarantinedHostsCache: string;
}

type EventTypesToEventPayload<T> = EventUnionFromDictionary<UndefinedAsNullAll<SerializedAll<T>>>;
Expand Down
21 changes: 20 additions & 1 deletion packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js';
import { isAnnounce, isPost, type IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
Expand Down Expand Up @@ -107,6 +107,23 @@ export class QueueService {
});
}

private isPublicContent(content: IActivity) {
let toPublicOnly = false;
if (isAnnounce(content)) {
penginn-net marked this conversation as resolved.
Show resolved Hide resolved
toPublicOnly = true;
}
if (typeof content.object !== 'string') {
if (isPost(content.object)) {
toPublicOnly = true;
}
}
if (toPublicOnly) {
return String(content.to) === 'https://www.w3.org/ns/activitystreams#Public' || String(content.cc) === 'https://www.w3.org/ns/activitystreams#Public';
} else {
return true;
}
}

@bindThis
public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) {
if (content == null) return null;
Expand All @@ -123,6 +140,7 @@ export class QueueService {
digest,
to,
isSharedInbox,
isPublicContent: this.isPublicContent(content),
};

return this.deliverQueue.add(to, data, {
Expand Down Expand Up @@ -165,6 +183,7 @@ export class QueueService {
digest,
to: d[0],
isSharedInbox: d[1],
isPublicContent: this.isPublicContent(content),
},
opts,
})));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class InstanceEntityService {
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
moderationNote: iAmModerator ? instance.moderationNote : null,
reversiVersion: instance.reversiVersion,
isQuarantineLimited: instance.quarantineLimited,
};
}

Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/models/Instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,12 @@ export class MiInstance {
length: 64, nullable: true,
})
public reversiVersion: string | null;
/**
* このインスタンスへの配送制限
*/
@Index()
@Column('boolean', {
default: false,
})
public quarantineLimited: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,9 @@ export const packedFederationInstanceSchema = {
type: 'string',
optional: true, nullable: true,
},
isQuarantineLimited: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;
41 changes: 41 additions & 0 deletions packages/backend/src/queue/processors/DeliverProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq';
import { Not } from 'typeorm';
import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { InstancesRepository, MiMeta } from '@/models/_.js';
import type Logger from '@/logger.js';
Expand All @@ -20,13 +21,15 @@ import FederationChart from '@/core/chart/charts/federation.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DeliverJobData } from '../types.js';

@Injectable()
export class DeliverProcessorService {
private logger: Logger;
private suspendedHostsCache: MemorySingleCache<MiInstance[]>;
private quarantinedHostsCache: MemorySingleCache<MiInstance[]>;
private latest: string | null;

constructor(
Expand All @@ -35,6 +38,8 @@ export class DeliverProcessorService {

@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,

private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
Expand All @@ -47,6 +52,25 @@ export class DeliverProcessorService {
) {
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
this.quarantinedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h
this.redisForSub.on('message', this.onMessage);
}

@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);

if (obj.channel === 'internal') {
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'clearQuarantinedHostsCache': {
this.quarantinedHostsCache.delete();
break;
}
default:
break;
}
}
}

@bindThis
Expand All @@ -70,6 +94,23 @@ export class DeliverProcessorService {
if (suspendedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) {
return 'skip (suspended)';
}
// isQuarantinedなら中断
let quarantinedHosts = this.quarantinedHostsCache.get();
if (quarantinedHosts == null) {
quarantinedHosts = await this.instancesRepository.find({
where: {
quarantineLimited: true,
},
});
this.quarantinedHostsCache.set(quarantinedHosts);
}
if (quarantinedHosts.map(x => x.host).includes(this.utilityService.toPuny(host))) {
console.log('quarantinedHosts public ? ' + job.data.isPublicContent);
console.log(job.data.content);
kozakura913 marked this conversation as resolved.
Show resolved Hide resolved
if (!job.data.isPublicContent) {
return 'skip (quarantined)';
}
}

try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/queue/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export type DeliverJobData = {
to: string;
/** whether it is sharedInbox */
isSharedInbox: boolean;
/** whether it is sharedInbox */
kozakura913 marked this conversation as resolved.
Show resolved Hide resolved
isPublicContent: boolean;
};

export type InboxJobData = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';

export const meta = {
tags: ['admin'],
Expand All @@ -25,6 +26,7 @@ export const paramDef = {
host: { type: 'string' },
isSuspended: { type: 'boolean' },
moderationNote: { type: 'string' },
isQuarantineLimit: { type: 'boolean' },
},
required: ['host'],
} as const;
Expand All @@ -34,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
private globalEventService: GlobalEventService,

private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
Expand All @@ -52,9 +55,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
}
const isQuarantineLimitBefore = instance.quarantineLimited;
let quarantineLimited: undefined | boolean;

if (ps.isQuarantineLimit != null && isQuarantineLimitBefore !== ps.isQuarantineLimit) {
quarantineLimited = ps.isQuarantineLimit;
}
const moderationNoteBefore = instance.moderationNote;
if ((!ps.moderationNote || moderationNoteBefore === ps.moderationNote) && (!quarantineLimited || isQuarantineLimitBefore === quarantineLimited) && (!suspensionState || isSuspendedBefore === ps.isSuspended)) {
//何も変更が無い時はupdateを呼ばない
//呼ぶとエラーが発生する
return;
}

await this.federatedInstanceService.update(instance.id, {
suspensionState,
quarantineLimited,
moderationNote: ps.moderationNote,
});

Expand All @@ -71,6 +87,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
}
if (ps.isQuarantineLimit != null && isQuarantineLimitBefore !== ps.isQuarantineLimit) {
if (ps.isQuarantineLimit) {
this.moderationLogService.log(me, 'quarantineRemoteInstance', {
id: instance.id,
host: instance.host,
});
} else {
this.moderationLogService.log(me, 'unquarantineRemoteInstance', {
id: instance.id,
host: instance.host,
});
}
this.globalEventService.publishInternalEvent('clearQuarantinedHostsCache', '');
}

if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
Expand Down
12 changes: 11 additions & 1 deletion packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ export const moderationLogTypes = [
'deleteGalleryPost',
'updateOfficialTags',
'unsetUserMutualLink',
'quarantineRemoteInstance',
'unquarantineRemoteInstance',
] as const;

export type ModerationLogPayloads = {
Expand Down Expand Up @@ -392,7 +394,15 @@ export type ModerationLogPayloads = {
userId: string;
userUsername: string;
userMutualLinkSections: { name: string | null; mutualLinks: { fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
}
};
quarantineRemoteInstance: {
id: string;
host: string;
};
unquarantineRemoteInstance: {
id: string;
host: string;
};
};

export type Serialized<T> = {
Expand Down
8 changes: 7 additions & 1 deletion packages/cherrypick-js/etc/cherrypick-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2706,10 +2706,16 @@ type ModerationLog = {
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
} | {
type: 'quarantineRemoteInstance';
info: ModerationLogPayloads['quarantineRemoteInstance'];
} | {
type: 'unquarantineRemoteInstance';
info: ModerationLogPayloads['unquarantineRemoteInstance'];
});

// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "quarantineRemoteInstance", "unquarantineRemoteInstance"];

// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
Expand Down
2 changes: 2 additions & 0 deletions packages/cherrypick-js/src/autogen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5073,6 +5073,7 @@ export type components = {
latestRequestReceivedAt: string | null;
moderationNote?: string | null;
reversiVersion?: string | null;
isQuarantineLimited: boolean;
};
GalleryPost: {
/**
Expand Down Expand Up @@ -8733,6 +8734,7 @@ export type operations = {
host: string;
isSuspended?: boolean;
moderationNote?: string;
isQuarantineLimit?: boolean;
};
};
};
Expand Down
10 changes: 10 additions & 0 deletions packages/cherrypick-js/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ export const moderationLogTypes = [
'deletePage',
'deleteFlash',
'deleteGalleryPost',
'quarantineRemoteInstance',
'unquarantineRemoteInstance',
] as const;

// See: packages/backend/src/core/ReversiService.ts@L410
Expand Down Expand Up @@ -439,4 +441,12 @@ export type ModerationLogPayloads = {
postUserUsername: string;
post: GalleryPost;
};
quarantineRemoteInstance: {
id: string;
host: string;
};
unquarantineRemoteInstance: {
id: string;
host: string;
};
};
6 changes: 6 additions & 0 deletions packages/cherrypick-js/src/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ export type ModerationLog = {
} | {
type: 'deleteGalleryPost';
info: ModerationLogPayloads['deleteGalleryPost'];
} | {
type: 'quarantineRemoteInstance';
info: ModerationLogPayloads['quarantineRemoteInstance'];
} | {
type: 'unquarantineRemoteInstance';
info: ModerationLogPayloads['unquarantineRemoteInstance'];
});

export type ServerStats = {
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend/src/pages/admin/modlog.ModLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
'markSensitiveDriveFile',
'resetPassword',
'suspendRemoteInstance',
'quarantineRemoteInstance',
].includes(log.type),
penginn-net marked this conversation as resolved.
Show resolved Hide resolved
[$style.logRed]: [
'suspend',
Expand Down Expand Up @@ -80,6 +81,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
<span v-else-if="log.type === 'quarantineRemoteInstance'">: {{ log.info.host }}</span>
<span v-else-if="log.type === 'unquarantineRemoteInstance'">: {{ log.info.host }}</span>
</template>
<template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
Expand Down
Loading
Loading