Merge branch 'feature-allowlist' into egirls-sharkey
This commit is contained in:
commit
74a62c2107
22 changed files with 158 additions and 5 deletions
|
@ -142,6 +142,7 @@ unmute: "Unmute"
|
||||||
renoteMute: "Mute Boosts"
|
renoteMute: "Mute Boosts"
|
||||||
renoteUnmute: "Unmute Boosts"
|
renoteUnmute: "Unmute Boosts"
|
||||||
block: "Block"
|
block: "Block"
|
||||||
|
allow: "Allow"
|
||||||
unblock: "Unblock"
|
unblock: "Unblock"
|
||||||
markAsNSFW: "Mark all media from user as NSFW"
|
markAsNSFW: "Mark all media from user as NSFW"
|
||||||
suspend: "Suspend"
|
suspend: "Suspend"
|
||||||
|
@ -210,6 +211,7 @@ perHour: "Per Hour"
|
||||||
perDay: "Per Day"
|
perDay: "Per Day"
|
||||||
stopActivityDelivery: "Stop sending activities"
|
stopActivityDelivery: "Stop sending activities"
|
||||||
blockThisInstance: "Block this instance"
|
blockThisInstance: "Block this instance"
|
||||||
|
allowThisInstance: "Allow this instance"
|
||||||
silenceThisInstance: "Silence this instance"
|
silenceThisInstance: "Silence this instance"
|
||||||
operations: "Operations"
|
operations: "Operations"
|
||||||
software: "Software"
|
software: "Software"
|
||||||
|
@ -230,6 +232,9 @@ clearCachedFiles: "Clear cache"
|
||||||
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
|
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
|
||||||
blockedInstances: "Blocked Instances"
|
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."
|
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"
|
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."
|
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"
|
muteAndBlock: "Mutes and Blocks"
|
||||||
|
@ -249,6 +254,7 @@ noCustomEmojis: "There are no emoji"
|
||||||
noJobs: "There are no jobs"
|
noJobs: "There are no jobs"
|
||||||
federating: "Federating"
|
federating: "Federating"
|
||||||
blocked: "Blocked"
|
blocked: "Blocked"
|
||||||
|
allowed: "Allowed"
|
||||||
suspended: "Suspended"
|
suspended: "Suspended"
|
||||||
all: "All"
|
all: "All"
|
||||||
subscribing: "Subscribing"
|
subscribing: "Subscribing"
|
||||||
|
|
17
packages/backend/migration/1635065559969-allowlist.js
Normal file
17
packages/backend/migration/1635065559969-allowlist.js
Normal file
|
@ -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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -42,6 +42,12 @@ export class UtilityService {
|
||||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
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
|
@bindThis
|
||||||
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
|
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
|
||||||
if (sensitiveWords.length === 0) return false;
|
if (sensitiveWords.length === 0) return false;
|
||||||
|
|
|
@ -269,7 +269,9 @@ export class ApInboxService {
|
||||||
|
|
||||||
// アナウンス先をブロックしてたら中断
|
// アナウンス先をブロックしてたら中断
|
||||||
const meta = await this.metaService.fetch();
|
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);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
|
|
|
@ -99,6 +99,10 @@ export class Resolver {
|
||||||
throw new Error('Instance is blocked');
|
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) {
|
if (this.config.signToActivityPubGet && !this.user) {
|
||||||
this.user = await this.instanceActorService.getInstanceActor();
|
this.user = await this.instanceActorService.getInstanceActor();
|
||||||
}
|
}
|
||||||
|
|
|
@ -532,7 +532,12 @@ export class ApNoteService {
|
||||||
|
|
||||||
// ブロックしていたら中断
|
// ブロックしていたら中断
|
||||||
const meta = await this.metaService.fetch();
|
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);
|
throw new StatusError('blocked host', 451);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,11 +61,13 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('f.followerHost')
|
.select('f.followerHost')
|
||||||
.where('f.followerHost IS NOT NULL');
|
.where('f.followerHost IS NOT NULL');
|
||||||
|
|
||||||
|
const skipAllowlistQuery = !meta.allowlistMode || meta.allowedHosts.length === 0;
|
||||||
const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([
|
const [sub, pub, pubsub, subActive, pubActive] = await Promise.all([
|
||||||
this.followingsRepository.createQueryBuilder('following')
|
this.followingsRepository.createQueryBuilder('following')
|
||||||
.select('COUNT(DISTINCT following.followeeHost)')
|
.select('COUNT(DISTINCT following.followeeHost)')
|
||||||
.where('following.followeeHost IS NOT NULL')
|
.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(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 NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
@ -73,6 +75,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(DISTINCT following.followerHost)')
|
.select('COUNT(DISTINCT following.followerHost)')
|
||||||
.where('following.followerHost IS NOT NULL')
|
.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(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() })`)
|
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
|
@ -80,6 +83,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(DISTINCT following.followeeHost)')
|
.select('COUNT(DISTINCT following.followeeHost)')
|
||||||
.where('following.followeeHost IS NOT NULL')
|
.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(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 NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||||
.setParameters(pubsubSubQuery.getParameters())
|
.setParameters(pubsubSubQuery.getParameters())
|
||||||
|
@ -89,6 +93,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
.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(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.isSuspended = false')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
@ -97,6 +102,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
||||||
.select('COUNT(instance.id)')
|
.select('COUNT(instance.id)')
|
||||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
.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(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.isSuspended = false')
|
||||||
.andWhere('instance.isNotResponding = false')
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
|
|
|
@ -35,6 +35,7 @@ export class InstanceEntityService {
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.isSuspended,
|
isSuspended: instance.isSuspended,
|
||||||
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
|
||||||
|
isAllowed: meta.allowlistMode ? this.utilityService.isAllowedHost(meta.allowedHosts, instance.host) : null,
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
openRegistrations: instance.openRegistrations,
|
openRegistrations: instance.openRegistrations,
|
||||||
|
|
|
@ -71,6 +71,16 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public blockedHosts: string[];
|
public blockedHosts: string[];
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024, array: true, default: '{}',
|
||||||
|
})
|
||||||
|
public allowedHosts: string[];
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public allowlistMode: boolean;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024, array: true, default: '{}',
|
length: 1024, array: true, default: '{}',
|
||||||
})
|
})
|
||||||
|
|
|
@ -49,6 +49,10 @@ export const packedFederationInstanceSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isAllowed: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
softwareName: {
|
softwareName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -53,9 +53,13 @@ export class DeliverProcessorService {
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const meta = await this.metaService.fetch();
|
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)';
|
return 'skip (blocked)';
|
||||||
}
|
}
|
||||||
|
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, punyHost)) {
|
||||||
|
return 'skip (not allowed)';
|
||||||
|
}
|
||||||
|
|
||||||
// isSuspendedなら中断
|
// isSuspendedなら中断
|
||||||
let suspendedHosts = this.suspendedHostsCache.get();
|
let suspendedHosts = this.suspendedHostsCache.get();
|
||||||
|
|
|
@ -67,6 +67,10 @@ export class InboxProcessorService {
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (meta.allowlistMode && !this.utilityService.isAllowedHost(meta.allowedHosts, host)) {
|
||||||
|
return `Blocked request: ${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
const keyIdLower = signature.keyId.toLowerCase();
|
const keyIdLower = signature.keyId.toLowerCase();
|
||||||
if (keyIdLower.startsWith('acct:')) {
|
if (keyIdLower.startsWith('acct:')) {
|
||||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||||
|
@ -159,6 +163,9 @@ export class InboxProcessorService {
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${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 {
|
} else {
|
||||||
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,6 +181,11 @@ export class ActivityPubServerService {
|
||||||
this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`);
|
this.authlogger.warn(`${request.id} ${request.url} instance ${keyHost} is blocked: refuse`);
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return true;
|
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?
|
// do we know the signer already?
|
||||||
|
|
|
@ -141,6 +141,17 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowedHosts: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowlistMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
sensitiveWords: {
|
sensitiveWords: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -503,6 +514,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
|
allowedHosts: instance.allowedHosts,
|
||||||
|
allowlistMode: instance.allowlistMode,
|
||||||
silencedHosts: instance.silencedHosts,
|
silencedHosts: instance.silencedHosts,
|
||||||
sensitiveWords: instance.sensitiveWords,
|
sensitiveWords: instance.sensitiveWords,
|
||||||
preservedUsernames: instance.preservedUsernames,
|
preservedUsernames: instance.preservedUsernames,
|
||||||
|
|
|
@ -36,6 +36,12 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowedHosts: {
|
||||||
|
type: 'array', nullable: true, items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowlistMode: { type: 'boolean', nullable: true },
|
||||||
sensitiveWords: {
|
sensitiveWords: {
|
||||||
type: 'array', nullable: true, items: {
|
type: 'array', nullable: true, items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -172,6 +178,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
|
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)) {
|
if (Array.isArray(ps.sensitiveWords)) {
|
||||||
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const fetchedMeta = await this.metaService.fetch();
|
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([
|
let local = await this.mergePack(me, ...await Promise.all([
|
||||||
this.apDbResolverService.getUserFromApId(uri),
|
this.apDbResolverService.getUserFromApId(uri),
|
||||||
|
|
|
@ -34,6 +34,7 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' },
|
host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' },
|
||||||
blocked: { type: 'boolean', nullable: true },
|
blocked: { type: 'boolean', nullable: true },
|
||||||
|
allowed: { type: 'boolean', nullable: true },
|
||||||
notResponding: { type: 'boolean', nullable: true },
|
notResponding: { type: 'boolean', nullable: true },
|
||||||
suspended: { type: 'boolean', nullable: true },
|
suspended: { type: 'boolean', nullable: true },
|
||||||
silenced: { type: 'boolean', nullable: true },
|
silenced: { type: 'boolean', nullable: true },
|
||||||
|
@ -107,6 +108,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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 (typeof ps.notResponding === 'boolean') {
|
||||||
if (ps.notResponding) {
|
if (ps.notResponding) {
|
||||||
query.andWhere('instance.isNotResponding = TRUE');
|
query.andWhere('instance.isNotResponding = TRUE');
|
||||||
|
|
|
@ -159,6 +159,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
if (note.user?.isSuspended) return false;
|
if (note.user?.isSuspended) return false;
|
||||||
if (this.utilityService.isBlockedHost(meta.blockedHosts, note.userHost)) 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;
|
if (this.utilityService.isSilencedHost(meta.silencedHosts, note.userHost)) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
<option value="silenced">{{ i18n.ts.silence }}</option>
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||||
|
<option value="allowed">{{ i18n.ts.allowed }}</option>
|
||||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkSelect v-model="sort">
|
<MkSelect v-model="sort">
|
||||||
|
@ -77,6 +78,7 @@ const pagination = {
|
||||||
state.value === 'publishing' ? { publishing: true } :
|
state.value === 'publishing' ? { publishing: true } :
|
||||||
state.value === 'suspended' ? { suspended: true } :
|
state.value === 'suspended' ? { suspended: true } :
|
||||||
state.value === 'blocked' ? { blocked: true } :
|
state.value === 'blocked' ? { blocked: true } :
|
||||||
|
state.value === 'allowed' ? { allowed: true } :
|
||||||
state.value === 'silenced' ? { silenced: true } :
|
state.value === 'silenced' ? { silenced: true } :
|
||||||
state.value === 'notResponding' ? { notResponding: true } :
|
state.value === 'notResponding' ? { notResponding: true } :
|
||||||
state.value === 'nsfw' ? { nsfw: true } :
|
state.value === 'nsfw' ? { nsfw: true } :
|
||||||
|
|
|
@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="nsfw">NSFW</option>
|
<option value="nsfw">NSFW</option>
|
||||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||||
|
<option value="allowed">{{ i18n.ts.allowed }}</option>
|
||||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
<option value="silenced">{{ i18n.ts.silence }}</option>
|
||||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
@ -85,6 +86,7 @@ const pagination = {
|
||||||
state.value === 'publishing' ? { publishing: true } :
|
state.value === 'publishing' ? { publishing: true } :
|
||||||
state.value === 'suspended' ? { suspended: true } :
|
state.value === 'suspended' ? { suspended: true } :
|
||||||
state.value === 'blocked' ? { blocked: true } :
|
state.value === 'blocked' ? { blocked: true } :
|
||||||
|
state.value === 'allowed' ? { allowed: true } :
|
||||||
state.value === 'silenced' ? { silenced: true } :
|
state.value === 'silenced' ? { silenced: true } :
|
||||||
state.value === 'notResponding' ? { notResponding: true } :
|
state.value === 'notResponding' ? { notResponding: true } :
|
||||||
state.value === 'nsfw' ? { nsfw: true } :
|
state.value === 'nsfw' ? { nsfw: true } :
|
||||||
|
@ -98,6 +100,7 @@ function getStatus(instance) {
|
||||||
if (instance.isSilenced) return 'Silenced';
|
if (instance.isSilenced) return 'Silenced';
|
||||||
if (instance.isNotResponding) return 'Error';
|
if (instance.isNotResponding) return 'Error';
|
||||||
if (instance.isNSFW) return 'NSFW';
|
if (instance.isNSFW) return 'NSFW';
|
||||||
|
if (instance.isAllowed) return 'Allowed';
|
||||||
return 'Alive';
|
return 'Alive';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span>{{ i18n.ts.silencedInstances }}</span>
|
<span>{{ i18n.ts.silencedInstances }}</span>
|
||||||
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
|
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
|
<template v-else-if="tab === 'allow'">
|
||||||
|
<MkSwitch v-model="allowlistMode">{{ i18n.ts.allowlistModeDescription }}</MkSwitch>
|
||||||
|
<br />
|
||||||
|
<MkTextarea v-model="allowedHosts" class="_formBlock">
|
||||||
|
<span>{{ i18n.ts.allowedInstances }}</span>
|
||||||
|
<template #caption>{{ i18n.ts.allowedInstancesDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
|
</template>
|
||||||
<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
<MkButton primary @click="save"><i class="ph-floppy-disk ph-bold ph-lg"></i> {{ i18n.ts.save }}</MkButton>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
|
@ -27,6 +35,7 @@ import { ref, computed } from 'vue';
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { fetchInstance } from '@/instance.js';
|
import { fetchInstance } from '@/instance.js';
|
||||||
|
@ -35,18 +44,24 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
|
|
||||||
const blockedHosts = ref<string>('');
|
const blockedHosts = ref<string>('');
|
||||||
const silencedHosts = ref<string>('');
|
const silencedHosts = ref<string>('');
|
||||||
const tab = ref('block');
|
const allowedHosts = ref<string>('');
|
||||||
|
const allowlistMode = ref<boolean>(false);
|
||||||
|
const tab = ref('allow');
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
blockedHosts.value = meta.blockedHosts.join('\n');
|
blockedHosts.value = meta.blockedHosts.join('\n');
|
||||||
silencedHosts.value = meta.silencedHosts.join('\n');
|
silencedHosts.value = meta.silencedHosts.join('\n');
|
||||||
|
allowedHosts.value = meta.allowedHosts.join('\n');
|
||||||
|
allowlistMode.value = meta.allowlistMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
blockedHosts: blockedHosts.value.split('\n') || [],
|
blockedHosts: blockedHosts.value.split('\n') || [],
|
||||||
silencedHosts: silencedHosts.value.split('\n') || [],
|
silencedHosts: silencedHosts.value.split('\n') || [],
|
||||||
|
allowedHosts: allowedHosts.value.split('\n') || [],
|
||||||
|
allowlistMode: allowlistMode.value || false,
|
||||||
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
|
@ -63,6 +78,10 @@ const headerTabs = computed(() => [{
|
||||||
key: 'silence',
|
key: 'silence',
|
||||||
title: i18n.ts.silence,
|
title: i18n.ts.silence,
|
||||||
icon: 'ph-eye-closed ph-bold ph-lg',
|
icon: 'ph-eye-closed ph-bold ph-lg',
|
||||||
|
}, {
|
||||||
|
key: 'allow',
|
||||||
|
title: i18n.ts.allow,
|
||||||
|
icon: 'ph-prohibit ph-bold ph-lg',
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
|
|
|
@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
|
||||||
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="isAllowed" :disabled="!meta || !instance" @update:modelValue="toggleAllow">{{ i18n.ts.allowThisInstance }}</MkSwitch>
|
||||||
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
|
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @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">Mark as NSFW</MkSwitch>
|
||||||
<MkButton @click="refreshMetadata"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
<MkButton @click="refreshMetadata"><i class="ph-arrows-counter-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
||||||
|
@ -149,6 +150,7 @@ const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
|
||||||
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
const instance = ref<Misskey.entities.FederationInstance | null>(null);
|
||||||
const suspended = ref(false);
|
const suspended = ref(false);
|
||||||
const isBlocked = ref(false);
|
const isBlocked = ref(false);
|
||||||
|
const isAllowed = ref(false);
|
||||||
const isSilenced = ref(false);
|
const isSilenced = ref(false);
|
||||||
const isNSFW = ref(false);
|
const isNSFW = ref(false);
|
||||||
const faviconUrl = ref<string | null>(null);
|
const faviconUrl = ref<string | null>(null);
|
||||||
|
@ -173,6 +175,7 @@ async function fetch(): Promise<void> {
|
||||||
});
|
});
|
||||||
suspended.value = instance.value?.isSuspended ?? false;
|
suspended.value = instance.value?.isSuspended ?? false;
|
||||||
isBlocked.value = instance.value?.isBlocked ?? false;
|
isBlocked.value = instance.value?.isBlocked ?? false;
|
||||||
|
isAllowed.value = instance.value?.isAllowed ?? 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;
|
||||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||||
|
@ -187,6 +190,15 @@ async function toggleBlock(): Promise<void> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleAllow(): Promise<void> {
|
||||||
|
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<void> {
|
async function toggleSilenced(): Promise<void> {
|
||||||
if (!meta.value) throw new Error('No meta?');
|
if (!meta.value) throw new Error('No meta?');
|
||||||
if (!instance.value) throw new Error('No instance?');
|
if (!instance.value) throw new Error('No instance?');
|
||||||
|
|
Loading…
Reference in a new issue