diff --git a/locales/en-US.yml b/locales/en-US.yml index 64f5d568eb..bd74075e37 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -142,6 +142,7 @@ unmute: "Unmute" renoteMute: "Mute Boosts" renoteUnmute: "Unmute Boosts" block: "Block" +allow: "Allow" unblock: "Unblock" markAsNSFW: "Mark all media from user as NSFW" suspend: "Suspend" @@ -210,6 +211,7 @@ perHour: "Per Hour" perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" +allowThisInstance: "Allow this instance" silenceThisInstance: "Silence this instance" operations: "Operations" software: "Software" @@ -230,6 +232,9 @@ clearCachedFiles: "Clear cache" clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?" blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." +allowedInstances: "Allowed instances" +allowedInstancesDescription: "List the hostnames of the instances you want to allow separated by linebreaks. Listed instances will be able to communicate with this instance if they are not already blocked." +allowlistModeDescription: "Enable allowlist mode, which will reject federation with any instances not on a preapproved list." silencedInstances: "Silenced instances" silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances." muteAndBlock: "Mutes and Blocks" @@ -249,6 +254,7 @@ noCustomEmojis: "There are no emoji" noJobs: "There are no jobs" federating: "Federating" blocked: "Blocked" +allowed: "Allowed" suspended: "Suspended" all: "All" subscribing: "Subscribing" diff --git a/packages/backend/migration/1635065559969-allowlist.js b/packages/backend/migration/1635065559969-allowlist.js new file mode 100644 index 0000000000..3a18c81139 --- /dev/null +++ b/packages/backend/migration/1635065559969-allowlist.js @@ -0,0 +1,17 @@ + +export class allowlist1635065559969 { + constructor() { + this.name = 'allowlist1635065559969'; + } + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "allowedHosts" character varying(256) array NOT NULL DEFAULT '{}'::varchar[]`); + await queryRunner.query(`ALTER TABLE "meta" ADD "allowlistMode" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowedHosts"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "allowlistMode"`); + } + +} diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts index 5dec36c89e..b040d061e5 100644 --- a/packages/backend/src/core/UtilityService.ts +++ b/packages/backend/src/core/UtilityService.ts @@ -42,6 +42,12 @@ export class UtilityService { return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); } + @bindThis + public isAllowedHost(allowedHosts: string[], host: string | null): boolean { + if (host == null) return false; + return allowedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`)); + } + @bindThis public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean { if (sensitiveWords.length === 0) return false; diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d8616d293d..1916bbe540 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -269,7 +269,9 @@ export class ApInboxService { // アナウンス先をブロックしてたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + const dbHost = this.utilityService.extractDbHost(uri); + if (this.utilityService.isBlockedHost(meta.blockedHosts, dbHost)) return; + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, dbHost)) return; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 870cf6372a..1e21647f5d 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -99,6 +99,10 @@ export class Resolver { throw new Error('Instance is blocked'); } + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, host)) { + throw new Error('Instance is blocked'); + } + if (this.config.signToActivityPubGet && !this.user) { this.user = await this.instanceActorService.getInstanceActor(); } diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 2595783e04..c28df27b63 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -532,7 +532,12 @@ export class ApNoteService { // ブロックしていたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + const dbHost = this.utilityService.extractDbHost(uri); + if (this.utilityService.isBlockedHost(meta.blockedHosts, dbHost)) { + throw new StatusError('blocked host', 451); + } + + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, dbHost)) { throw new StatusError('blocked host', 451); } diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index fc474b002b..0d3f1b3f2c 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -61,11 +61,13 @@ export default class FederationChart extends Chart { // eslint-di .select('f.followerHost') .where('f.followerHost IS NOT NULL'); + const skipAllowlistQuery = !meta.allowlistMode || meta.allowedHosts.length === 0; const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([ this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(skipAllowlistQuery ? '1=1' : 'following.followeeHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), @@ -73,6 +75,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(skipAllowlistQuery ? '1=1' : 'following.followerHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), @@ -80,6 +83,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(skipAllowlistQuery ? '1=1' : 'following.followeeHost ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -89,6 +93,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(skipAllowlistQuery ? '1=1' : 'instance.host ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -97,6 +102,7 @@ export default class FederationChart extends Chart { // eslint-di .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(skipAllowlistQuery ? '1=1' : 'instance.host ILIKE ANY(ARRAY[:...allowed])', { allowed: meta.allowedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.isSuspended = false') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 515b356dee..64dfd3b5d6 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -35,6 +35,7 @@ export class InstanceEntityService { isNotResponding: instance.isNotResponding, isSuspended: instance.isSuspended, isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), + isAllowed: meta.allowlistMode ? this.utilityService.isAllowedHost(meta.allowedHosts, instance.host) : null, softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 4bf856e619..84ab18759b 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -71,6 +71,16 @@ export class MiMeta { }) public blockedHosts: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public allowedHosts: string[]; + + @Column('boolean', { + default: false, + }) + public allowlistMode: boolean; + @Column('varchar', { length: 1024, array: true, default: '{}', }) diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 94873716bf..8042d0fe5b 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -49,6 +49,10 @@ export const packedFederationInstanceSchema = { type: 'boolean', optional: false, nullable: false, }, + isAllowed: { + type: 'boolean', + optional: false, nullable: true, + }, softwareName: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4a1d9f28b4..b419134d3d 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -53,9 +53,13 @@ export class DeliverProcessorService { // ブロックしてたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) { + const punyHost = this.utilityService.toPuny(host); + if (this.utilityService.isBlockedHost(meta.blockedHosts, punyHost)) { return 'skip (blocked)'; } + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, punyHost)) { + return 'skip (not allowed)'; + } // isSuspendedなら中断 let suspendedHosts = this.suspendedHostsCache.get(); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f69634968d..1d89912bf6 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -67,6 +67,10 @@ export class InboxProcessorService { return `Blocked request: ${host}`; } + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, host)) { + return `Blocked request: ${host}`; + } + const keyIdLower = signature.keyId.toLowerCase(); if (keyIdLower.startsWith('acct:')) { return `Old keyId is no longer supported. ${keyIdLower}`; @@ -159,6 +163,9 @@ export class InboxProcessorService { if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, ldHost)) { + throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); + } } else { throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`); } diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 8fa8320c8c..722a17e956 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -181,6 +181,11 @@ export class ActivityPubServerService { this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`); reply.code(401); return true; + } else if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, keyHost)) { + /* allowlist mode enabled and instance not on allowlist: refuse */ + this.authLogger.warn(`${request.id} ${request.url} instance ${keyHost} is not on allowlist: refuse`); + reply.code(401); + return true; } // do we know the signer already? diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 4fd2a568ad..0ef7124d91 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -141,6 +141,17 @@ export const meta = { type: 'string', }, }, + allowedHosts: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, + allowlistMode: { + type: 'boolean', + optional: false, nullable: false, + }, sensitiveWords: { type: 'array', optional: false, nullable: false, @@ -503,6 +514,8 @@ export default class extends Endpoint { // eslint- pinnedUsers: instance.pinnedUsers, hiddenTags: instance.hiddenTags, blockedHosts: instance.blockedHosts, + allowedHosts: instance.allowedHosts, + allowlistMode: instance.allowlistMode, silencedHosts: instance.silencedHosts, sensitiveWords: instance.sensitiveWords, 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 5c916fe340..1c792fa9c1 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -36,6 +36,12 @@ export const paramDef = { type: 'string', }, }, + allowedHosts: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, + allowlistMode: { type: 'boolean', nullable: true }, sensitiveWords: { type: 'array', nullable: true, items: { type: 'string', @@ -172,6 +178,14 @@ export default class extends Endpoint { // eslint- set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (Array.isArray(ps.allowedHosts)) { + set.allowedHosts = ps.allowedHosts.filter(Boolean).map(x => x.toLowerCase()); + } + + if (ps.allowlistMode !== undefined) { + set.allowlistMode = ps.allowlistMode; + } + if (Array.isArray(ps.sensitiveWords)) { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 8ab16880fa..fb4434dc7a 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -114,7 +114,9 @@ export default class extends Endpoint { // eslint- private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { // ブロックしてたら中断 const fetchedMeta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; + const dbHost = this.utilityService.extractDbHost(uri); + if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, dbHost)) return null; + if (fetchedMeta.allowlistMode && !this.utilityService.isAllowedHost(fetchedMeta.allowedHosts, dbHost)) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index 617ca65733..f5c1ff6920 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -34,6 +34,7 @@ export const paramDef = { properties: { host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' }, blocked: { type: 'boolean', nullable: true }, + allowed: { type: 'boolean', nullable: true }, notResponding: { type: 'boolean', nullable: true }, suspended: { type: 'boolean', nullable: true }, silenced: { type: 'boolean', nullable: true }, @@ -107,6 +108,15 @@ export default class extends Endpoint { // eslint- } } + if (typeof ps.allowed === 'boolean') { + const meta = await this.metaService.fetch(true); + if (ps.allowed) { + query.andWhere(meta.allowedHosts.length === 0 ? '1=0' : 'instance.host IN (:...allows)', { allows: meta.allowedHosts }); + } else { + query.andWhere(meta.allowedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...allows)', { allows: meta.allowedHosts }); + } + } + if (typeof ps.notResponding === 'boolean') { if (ps.notResponding) { query.andWhere('instance.isNotResponding = TRUE'); diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 89e05fd57e..e6c8d2b816 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -157,6 +157,7 @@ export default class extends Endpoint { // eslint- if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false; if (note.user?.isSuspended) return false; if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) return false; + if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, note.userHost)) return false; if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false; return true; }); diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index 0de000ee3e..64f07014a9 100644 --- a/packages/frontend/src/pages/about.federation.vue +++ b/packages/frontend/src/pages/about.federation.vue @@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -77,6 +78,7 @@ const pagination = { state.value === 'publishing' ? { publishing: true } : state.value === 'suspended' ? { suspended: true } : state.value === 'blocked' ? { blocked: true } : + state.value === 'allowed' ? { allowed: true } : state.value === 'silenced' ? { silenced: true } : state.value === 'notResponding' ? { notResponding: true } : state.value === 'nsfw' ? { nsfw: true } : diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 1888a0eb16..cb6737b989 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only + @@ -85,6 +86,7 @@ const pagination = { state.value === 'publishing' ? { publishing: true } : state.value === 'suspended' ? { suspended: true } : state.value === 'blocked' ? { blocked: true } : + state.value === 'allowed' ? { allowed: true } : state.value === 'silenced' ? { silenced: true } : state.value === 'notResponding' ? { notResponding: true } : state.value === 'nsfw' ? { nsfw: true } : @@ -98,6 +100,7 @@ function getStatus(instance) { if (instance.isSilenced) return 'Silenced'; if (instance.isNotResponding) return 'Error'; if (instance.isNSFW) return 'NSFW'; + if (instance.isAllowed) return 'Allowed'; return 'Alive'; } diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue index e54f6dc065..e65ddff335 100644 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -16,6 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.silencedInstances }} + {{ i18n.ts.save }} @@ -27,6 +35,7 @@ import { ref, computed } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; import { fetchInstance } from '@/instance.js'; @@ -35,18 +44,24 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; const blockedHosts = ref(''); const silencedHosts = ref(''); -const tab = ref('block'); +const allowedHosts = ref(''); +const allowlistMode = ref(false); +const tab = ref('allow'); async function init() { const meta = await os.api('admin/meta'); blockedHosts.value = meta.blockedHosts.join('\n'); silencedHosts.value = meta.silencedHosts.join('\n'); + allowedHosts.value = meta.allowedHosts.join('\n'); + allowlistMode.value = meta.allowlistMode; } function save() { os.apiWithDialog('admin/update-meta', { blockedHosts: blockedHosts.value.split('\n') || [], silencedHosts: silencedHosts.value.split('\n') || [], + allowedHosts: allowedHosts.value.split('\n') || [], + allowlistMode: allowlistMode.value || false, }).then(() => { fetchInstance(); @@ -63,6 +78,10 @@ const headerTabs = computed(() => [{ key: 'silence', title: i18n.ts.silence, icon: 'ph-eye-closed ph-bold ph-lg', +}, { + key: 'allow', + title: i18n.ts.allow, + icon: 'ph-prohibit ph-bold ph-lg', }]); definePageMetadata({ diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue index 683a31c36d..14ea9dea46 100644 --- a/packages/frontend/src/pages/instance-info.vue +++ b/packages/frontend/src/pages/instance-info.vue @@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.stopActivityDelivery }} {{ i18n.ts.blockThisInstance }} + {{ i18n.ts.allowThisInstance }} {{ i18n.ts.silenceThisInstance }} Mark as NSFW Refresh metadata @@ -149,6 +150,7 @@ const meta = ref(null); const instance = ref(null); const suspended = ref(false); const isBlocked = ref(false); +const isAllowed = ref(false); const isSilenced = ref(false); const isNSFW = ref(false); const faviconUrl = ref(null); @@ -173,6 +175,7 @@ async function fetch(): Promise { }); suspended.value = instance.value?.isSuspended ?? false; isBlocked.value = instance.value?.isBlocked ?? false; + isAllowed.value = instance.value?.isAllowed ?? false; isSilenced.value = instance.value?.isSilenced ?? false; isNSFW.value = instance.value?.isNSFW ?? false; faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); @@ -187,6 +190,15 @@ async function toggleBlock(): Promise { }); } +async function toggleAllow(): Promise { + if (!meta.value) throw new Error('No meta?'); + if (!instance.value) throw new Error('No instance?'); + const { host } = instance.value; + await os.api('admin/update-meta', { + allowedHosts: isAllowed.value ? meta.value.allowedHosts.concat([host]) : meta.value.allowedHosts.filter(x => x !== host), + }); +} + async function toggleSilenced(): Promise { if (!meta.value) throw new Error('No meta?'); if (!instance.value) throw new Error('No instance?');