diff --git a/packages/backend/migration/1697247230117-InstanceSilence.js b/packages/backend/migration/1697247230117-InstanceSilence.js new file mode 100644 index 0000000000..bf8a1443eb --- /dev/null +++ b/packages/backend/migration/1697247230117-InstanceSilence.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class InstanceSilence1697247230117 { + name = 'InstanceSilence1697247230117' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "silencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "flash" ALTER COLUMN "visibility" DROP NOT NULL`); + } +} diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index beffcc2e9c..0a314c1193 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { IsNull } from 'typeorm'; +import {DataSource, IsNull } from 'typeorm'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueService } from '@/core/QueueService.js'; @@ -29,6 +29,8 @@ import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import Logger from '../logger.js'; +import { shouldSilenceInstance } from "@/misc/should-block-instance.js"; + const logger = new Logger('following/create'); @@ -52,6 +54,9 @@ export class UserFollowingService implements OnModuleInit { constructor( private moduleRef: ModuleRef, + @Inject(DI.db) + private db: DataSource, + @Inject(DI.config) private config: Config, @@ -121,12 +126,14 @@ export class UserFollowingService implements OnModuleInit { // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or - // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである + // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or + // フォロワーがローカルユーザーであり、フォロー対象がサイレンスされているサーバーである // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく if ( followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || - (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') + (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || + ( this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && await shouldSilenceInstance(follower.host,this.db)) ) { let autoAccept = false; diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 0e27e9df7f..15c1d07c1b 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -3,17 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import {Inject, Injectable} from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import type { } from '@/models/Blocking.js'; import type { MiInstance } from '@/models/Instance.js'; import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '../UtilityService.js'; +import {shouldSilenceInstance} from "@/misc/should-block-instance.js"; +import { DataSource } from 'typeorm'; +import {DI} from "@/di-symbols.js"; @Injectable() export class InstanceEntityService { constructor( + @Inject(DI.db) + private db: DataSource, + private metaService: MetaService, private utilityService: UtilityService, @@ -43,6 +49,7 @@ export class InstanceEntityService { description: instance.description, maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, + isSilenced: await shouldSilenceInstance(instance.host,this.db), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, diff --git a/packages/backend/src/misc/fetch-meta.ts b/packages/backend/src/misc/fetch-meta.ts new file mode 100644 index 0000000000..99964a300a --- /dev/null +++ b/packages/backend/src/misc/fetch-meta.ts @@ -0,0 +1,40 @@ +import { DataSource } from 'typeorm'; +import { MiMeta } from "@/models/Meta.js"; + +let cache: MiMeta; + +export async function fetchMeta(noCache = false , db: DataSource): Promise { + if (!noCache && cache) return cache; + + return await db.transaction(async (transactionalEntityManager) => { + // New IDs are prioritized because multiple records may have been created due to past bugs. + const metas = await transactionalEntityManager.find(MiMeta, { + order: { + id: "DESC", + }, + }); + + const meta = metas[0]; + + if (meta) { + cache = meta; + return meta; + } else { + // If fetchMeta is called at the same time when meta is empty, this part may be called at the same time, so use fail-safe upsert. + const saved = await transactionalEntityManager + .upsert( + MiMeta, + { + id: "x", + }, + ["id"], + ) + .then((x) => + transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]), + ); + + cache = saved; + return saved; + } + }); +} diff --git a/packages/backend/src/misc/should-block-instance.ts b/packages/backend/src/misc/should-block-instance.ts new file mode 100644 index 0000000000..0c38d3867e --- /dev/null +++ b/packages/backend/src/misc/should-block-instance.ts @@ -0,0 +1,15 @@ +import { fetchMeta } from "@/misc/fetch-meta.js"; +import type { MiInstance } from "@/models/Instance.js"; +import type { MiMeta } from "@/models/Meta.js"; +import { DataSource } from "typeorm"; + +export async function shouldSilenceInstance( + host: MiInstance["host"], + db : DataSource, + meta?: MiMeta, +): Promise { + const { silencedHosts } = meta ?? (await fetchMeta(true,db)); + return silencedHosts.some( + (limitedHost: string) => host === limitedHost || host.endsWith(`.${limitedHost}`), + ); +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index d2bd0c26e9..23ae513ede 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -76,6 +76,11 @@ export class MiMeta { }) public sensitiveWords: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public silencedHosts: 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 ac07519f16..4ad84d02ff 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -93,6 +93,11 @@ export const packedFederationInstanceSchema = { type: 'string', optional: false, nullable: true, }, + isSilenced: { + type: "boolean", + optional: false, + nullable: false, + }, infoUpdatedAt: { 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 5a74456ab0..f294934344 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -105,6 +105,16 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + silencedHosts: { + type: "array", + optional: true, + nullable: false, + items: { + type: "string", + optional: false, + nullable: false, + }, + }, pinnedUsers: { type: 'array', optional: false, nullable: false, @@ -367,6 +377,7 @@ export default class extends Endpoint { // eslint- pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, + silencedHosts: instance.silencedHosts, sensitiveWords: instance.sensitiveWords, preservedUsernames: instance.preservedUsernames, hcaptchaSecretKey: instance.hcaptchaSecretKey, 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 7db25e659f..c2d1f19b89 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -3,11 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; -import type { MiMeta } from '@/models/Meta.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; -import { Endpoint } from '@/server/api/endpoint-base.js'; -import { MetaService } from '@/core/MetaService.js'; +import {Injectable} from '@nestjs/common'; +import type {MiMeta} from '@/models/Meta.js'; +import {ModerationLogService} from '@/core/ModerationLogService.js'; +import {Endpoint} from '@/server/api/endpoint-base.js'; +import {MetaService} from '@/core/MetaService.js'; export const meta = { tags: ['admin'], @@ -19,102 +19,119 @@ export const meta = { export const paramDef = { type: 'object', properties: { - disableRegistration: { type: 'boolean', nullable: true }, - pinnedUsers: { type: 'array', nullable: true, items: { - type: 'string', - } }, - hiddenTags: { type: 'array', nullable: true, items: { - type: 'string', - } }, - blockedHosts: { type: 'array', nullable: true, items: { - type: 'string', - } }, - sensitiveWords: { type: 'array', nullable: true, items: { - type: 'string', - } }, - themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, - mascotImageUrl: { type: 'string', nullable: true }, - bannerUrl: { type: 'string', nullable: true }, - serverErrorImageUrl: { type: 'string', nullable: true }, - infoImageUrl: { type: 'string', nullable: true }, - notFoundImageUrl: { type: 'string', nullable: true }, - iconUrl: { type: 'string', nullable: true }, - app192IconUrl: { type: 'string', nullable: true }, - app512IconUrl: { type: 'string', nullable: true }, - backgroundImageUrl: { type: 'string', nullable: true }, - logoImageUrl: { type: 'string', nullable: true }, - name: { type: 'string', nullable: true }, - shortName: { type: 'string', nullable: true }, - description: { type: 'string', nullable: true }, - defaultLightTheme: { type: 'string', nullable: true }, - defaultDarkTheme: { type: 'string', nullable: true }, - cacheRemoteFiles: { type: 'boolean' }, - cacheRemoteSensitiveFiles: { type: 'boolean' }, - emailRequiredForSignup: { type: 'boolean' }, - enableHcaptcha: { type: 'boolean' }, - hcaptchaSiteKey: { type: 'string', nullable: true }, - hcaptchaSecretKey: { type: 'string', nullable: true }, - enableRecaptcha: { type: 'boolean' }, - recaptchaSiteKey: { type: 'string', nullable: true }, - recaptchaSecretKey: { type: 'string', nullable: true }, - enableTurnstile: { type: 'boolean' }, - turnstileSiteKey: { type: 'string', nullable: true }, - turnstileSecretKey: { type: 'string', nullable: true }, - sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, - sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, - setSensitiveFlagAutomatically: { type: 'boolean' }, - enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, - proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, - maintainerName: { type: 'string', nullable: true }, - maintainerEmail: { type: 'string', nullable: true }, - langs: { type: 'array', items: { - type: 'string', - } }, - summalyProxy: { type: 'string', nullable: true }, - deeplAuthKey: { type: 'string', nullable: true }, - deeplIsPro: { type: 'boolean' }, - enableEmail: { type: 'boolean' }, - email: { type: 'string', nullable: true }, - smtpSecure: { type: 'boolean' }, - smtpHost: { type: 'string', nullable: true }, - smtpPort: { type: 'integer', nullable: true }, - smtpUser: { type: 'string', nullable: true }, - smtpPass: { type: 'string', nullable: true }, - enableServiceWorker: { type: 'boolean' }, - swPublicKey: { type: 'string', nullable: true }, - swPrivateKey: { type: 'string', nullable: true }, - tosUrl: { type: 'string', nullable: true }, - repositoryUrl: { type: 'string' }, - feedbackUrl: { type: 'string' }, - impressumUrl: { type: 'string' }, - privacyPolicyUrl: { type: 'string' }, - useObjectStorage: { type: 'boolean' }, - objectStorageBaseUrl: { type: 'string', nullable: true }, - objectStorageBucket: { type: 'string', nullable: true }, - objectStoragePrefix: { type: 'string', nullable: true }, - objectStorageEndpoint: { type: 'string', nullable: true }, - objectStorageRegion: { type: 'string', nullable: true }, - objectStoragePort: { type: 'integer', nullable: true }, - objectStorageAccessKey: { type: 'string', nullable: true }, - objectStorageSecretKey: { type: 'string', nullable: true }, - objectStorageUseSSL: { type: 'boolean' }, - objectStorageUseProxy: { type: 'boolean' }, - objectStorageSetPublicRead: { type: 'boolean' }, - objectStorageS3ForcePathStyle: { type: 'boolean' }, - enableIpLogging: { type: 'boolean' }, - enableActiveEmailValidation: { type: 'boolean' }, - enableChartsForRemoteUser: { type: 'boolean' }, - enableChartsForFederatedInstances: { type: 'boolean' }, - enableServerMachineStats: { type: 'boolean' }, - enableIdenticonGeneration: { type: 'boolean' }, - serverRules: { type: 'array', items: { type: 'string' } }, - preservedUsernames: { type: 'array', items: { type: 'string' } }, - manifestJsonOverride: { type: 'string' }, - perLocalUserUserTimelineCacheMax: { type: 'integer' }, - perRemoteUserUserTimelineCacheMax: { type: 'integer' }, - perUserHomeTimelineCacheMax: { type: 'integer' }, - perUserListTimelineCacheMax: { type: 'integer' }, - notesPerOneAd: { type: 'integer' }, + disableRegistration: {type: 'boolean', nullable: true}, + pinnedUsers: { + type: 'array', nullable: true, items: { + type: 'string', + } + }, + hiddenTags: { + type: 'array', nullable: true, items: { + type: 'string', + } + }, + blockedHosts: { + type: 'array', nullable: true, items: { + type: 'string', + } + }, + sensitiveWords: { + type: 'array', nullable: true, items: { + type: 'string', + } + }, + themeColor: {type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$'}, + mascotImageUrl: {type: 'string', nullable: true}, + bannerUrl: {type: 'string', nullable: true}, + serverErrorImageUrl: {type: 'string', nullable: true}, + infoImageUrl: {type: 'string', nullable: true}, + notFoundImageUrl: {type: 'string', nullable: true}, + iconUrl: {type: 'string', nullable: true}, + app192IconUrl: {type: 'string', nullable: true}, + app512IconUrl: {type: 'string', nullable: true}, + backgroundImageUrl: {type: 'string', nullable: true}, + logoImageUrl: {type: 'string', nullable: true}, + name: {type: 'string', nullable: true}, + shortName: {type: 'string', nullable: true}, + description: {type: 'string', nullable: true}, + defaultLightTheme: {type: 'string', nullable: true}, + defaultDarkTheme: {type: 'string', nullable: true}, + cacheRemoteFiles: {type: 'boolean'}, + cacheRemoteSensitiveFiles: {type: 'boolean'}, + emailRequiredForSignup: {type: 'boolean'}, + enableHcaptcha: {type: 'boolean'}, + hcaptchaSiteKey: {type: 'string', nullable: true}, + hcaptchaSecretKey: {type: 'string', nullable: true}, + enableRecaptcha: {type: 'boolean'}, + recaptchaSiteKey: {type: 'string', nullable: true}, + recaptchaSecretKey: {type: 'string', nullable: true}, + enableTurnstile: {type: 'boolean'}, + turnstileSiteKey: {type: 'string', nullable: true}, + turnstileSecretKey: {type: 'string', nullable: true}, + sensitiveMediaDetection: {type: 'string', enum: ['none', 'all', 'local', 'remote']}, + sensitiveMediaDetectionSensitivity: {type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh']}, + setSensitiveFlagAutomatically: {type: 'boolean'}, + enableSensitiveMediaDetectionForVideos: {type: 'boolean'}, + proxyAccountId: {type: 'string', format: 'misskey:id', nullable: true}, + maintainerName: {type: 'string', nullable: true}, + maintainerEmail: {type: 'string', nullable: true}, + langs: { + type: 'array', items: { + type: 'string', + } + }, + summalyProxy: {type: 'string', nullable: true}, + deeplAuthKey: {type: 'string', nullable: true}, + deeplIsPro: {type: 'boolean'}, + enableEmail: {type: 'boolean'}, + email: {type: 'string', nullable: true}, + smtpSecure: {type: 'boolean'}, + smtpHost: {type: 'string', nullable: true}, + smtpPort: {type: 'integer', nullable: true}, + smtpUser: {type: 'string', nullable: true}, + smtpPass: {type: 'string', nullable: true}, + enableServiceWorker: {type: 'boolean'}, + swPublicKey: {type: 'string', nullable: true}, + swPrivateKey: {type: 'string', nullable: true}, + tosUrl: {type: 'string', nullable: true}, + repositoryUrl: {type: 'string'}, + feedbackUrl: {type: 'string'}, + impressumUrl: {type: 'string'}, + privacyPolicyUrl: {type: 'string'}, + useObjectStorage: {type: 'boolean'}, + objectStorageBaseUrl: {type: 'string', nullable: true}, + objectStorageBucket: {type: 'string', nullable: true}, + objectStoragePrefix: {type: 'string', nullable: true}, + objectStorageEndpoint: {type: 'string', nullable: true}, + objectStorageRegion: {type: 'string', nullable: true}, + objectStoragePort: {type: 'integer', nullable: true}, + objectStorageAccessKey: {type: 'string', nullable: true}, + objectStorageSecretKey: {type: 'string', nullable: true}, + objectStorageUseSSL: {type: 'boolean'}, + objectStorageUseProxy: {type: 'boolean'}, + objectStorageSetPublicRead: {type: 'boolean'}, + objectStorageS3ForcePathStyle: {type: 'boolean'}, + enableIpLogging: {type: 'boolean'}, + enableActiveEmailValidation: {type: 'boolean'}, + enableChartsForRemoteUser: {type: 'boolean'}, + enableChartsForFederatedInstances: {type: 'boolean'}, + enableServerMachineStats: {type: 'boolean'}, + enableIdenticonGeneration: {type: 'boolean'}, + serverRules: {type: 'array', items: {type: 'string'}}, + preservedUsernames: {type: 'array', items: {type: 'string'}}, + manifestJsonOverride: {type: 'string'}, + perLocalUserUserTimelineCacheMax: {type: 'integer'}, + perRemoteUserUserTimelineCacheMax: {type: 'integer'}, + perUserHomeTimelineCacheMax: {type: 'integer'}, + perUserListTimelineCacheMax: {type: 'integer'}, + notesPerOneAd: {type: 'integer'}, + silencedHosts: { + type: "array", + nullable: true, + items: { + type: "string", + }, + }, }, required: [], } as const; @@ -147,7 +164,14 @@ export default class extends Endpoint { // 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); + }); + } if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index be73e5dbb8..c8beefa9c7 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -36,6 +36,7 @@ export const paramDef = { blocked: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, + silenced: { type: "boolean", nullable: true }, federating: { type: 'boolean', nullable: true }, subscribing: { type: 'boolean', nullable: true }, publishing: { type: 'boolean', nullable: true }, @@ -102,6 +103,23 @@ export default class extends Endpoint { // eslint- } } + if (typeof ps.silenced === "boolean") { + const meta = await this.metaService.fetch(true); + + if (ps.silenced) { + if (meta.silencedHosts.length === 0) { + return []; + } + query.andWhere("instance.host IN (:...silences)", { + silences: meta.silencedHosts, + }); + } else if (meta.silencedHosts.length > 0) { + query.andWhere("instance.host NOT IN (:...silences)", { + silences: meta.silencedHosts, + }); + } + } + if (typeof ps.federating === 'boolean') { if (ps.federating) { query.andWhere('((instance.followingCount > 0) OR (instance.followersCount > 0))'); diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue index de726e3aa4..6ca4266658 100644 --- a/packages/frontend/src/components/MkInstanceCardMini.vue +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->