merge: Persisted instance blocks (!1068)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1068 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
e1504cfb88
56 changed files with 854 additions and 157 deletions
|
|
@ -136,10 +136,10 @@ export class FanoutTimelineEndpointService {
|
|||
const parentFilter = filter;
|
||||
filter = (note) => {
|
||||
if (!ps.ignoreAuthorFromInstanceBlock) {
|
||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, note.userHost)) return false;
|
||||
if (note.userInstance?.isBlocked) return false;
|
||||
}
|
||||
if (note.userId !== note.renoteUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.renoteUserHost)) return false;
|
||||
if (note.userId !== note.replyUserId && this.utilityService.isBlockedHost(this.meta.blockedHosts, note.replyUserHost)) return false;
|
||||
if (note.userId !== note.renoteUserId && note.renoteUserInstance?.isBlocked) return false;
|
||||
if (note.userId !== note.replyUserId && note.replyUserInstance?.isBlocked) return false;
|
||||
|
||||
return parentFilter(note);
|
||||
};
|
||||
|
|
@ -194,7 +194,10 @@ export class FanoutTimelineEndpointService {
|
|||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
.leftJoinAndSelect('note.channel', 'channel')
|
||||
.leftJoinAndSelect('note.userInstance', 'userInstance')
|
||||
.leftJoinAndSelect('note.replyUserInstance', 'replyUserInstance')
|
||||
.leftJoinAndSelect('note.renoteUserInstance', 'renoteUserInstance');
|
||||
|
||||
const notes = (await query.getMany()).filter(noteFilter);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,23 +5,24 @@
|
|||
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
import type { InstancesRepository } from '@/models/_.js';
|
||||
import type { InstancesRepository, MiMeta } from '@/models/_.js';
|
||||
import type { MiInstance } from '@/models/Instance.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { diffArrays, diffArraysSimple } from '@/misc/diff-arrays.js';
|
||||
|
||||
@Injectable()
|
||||
export class FederatedInstanceService implements OnApplicationShutdown {
|
||||
public federatedInstanceCache: RedisKVCache<MiInstance | null>;
|
||||
private readonly federatedInstanceCache: MemoryKVCache<MiInstance | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
@Inject(DI.redisForSub)
|
||||
private redisForSub: Redis.Redis,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
|
@ -29,67 +30,46 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.federatedInstanceCache = new RedisKVCache<MiInstance | null>(this.redisClient, 'federatedInstance', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
|
||||
toRedisConverter: (value) => JSON.stringify(value),
|
||||
fromRedisConverter: (value) => {
|
||||
const parsed = JSON.parse(value);
|
||||
if (parsed == null) return null;
|
||||
return {
|
||||
...parsed,
|
||||
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||
notRespondingSince: parsed.notRespondingSince ? new Date(parsed.notRespondingSince) : null,
|
||||
};
|
||||
},
|
||||
});
|
||||
this.federatedInstanceCache = new MemoryKVCache(1000 * 60 * 3); // 3m
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchOrRegister(host: string): Promise<MiInstance> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
const cached = this.federatedInstanceCache.get(host);
|
||||
if (cached) return cached;
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
let index = await this.instancesRepository.findOneBy({ host });
|
||||
if (index == null) {
|
||||
let i;
|
||||
try {
|
||||
i = await this.instancesRepository.insertOne({
|
||||
await this.instancesRepository.createQueryBuilder('instance')
|
||||
.insert()
|
||||
.values({
|
||||
id: this.idService.gen(),
|
||||
host,
|
||||
firstRetrievedAt: new Date(),
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof QueryFailedError) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
i = await this.instancesRepository.findOneBy({ host });
|
||||
}
|
||||
}
|
||||
isBlocked: this.utilityService.isBlockedHost(host),
|
||||
isSilenced: this.utilityService.isSilencedHost(host),
|
||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(host),
|
||||
isAllowListed: this.utilityService.isAllowListedHost(host),
|
||||
isBubbled: this.utilityService.isBubbledHost(host),
|
||||
})
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
if (i == null) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
this.federatedInstanceCache.set(host, i);
|
||||
return i;
|
||||
} else {
|
||||
this.federatedInstanceCache.set(host, index);
|
||||
return index;
|
||||
index = await this.instancesRepository.findOneByOrFail({ host });
|
||||
}
|
||||
|
||||
this.federatedInstanceCache.set(host, index);
|
||||
return index;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<MiInstance | null> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
const cached = this.federatedInstanceCache.get(host);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
|
@ -117,8 +97,35 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
this.federatedInstanceCache.set(result.host, result);
|
||||
}
|
||||
|
||||
private syncCache(before: Serialized<MiMeta | undefined>, after: Serialized<MiMeta>): void {
|
||||
const changed =
|
||||
diffArraysSimple(before?.blockedHosts, after.blockedHosts) ||
|
||||
diffArraysSimple(before?.silencedHosts, after.silencedHosts) ||
|
||||
diffArraysSimple(before?.mediaSilencedHosts, after.mediaSilencedHosts) ||
|
||||
diffArraysSimple(before?.federationHosts, after.federationHosts) ||
|
||||
diffArraysSimple(before?.bubbleInstances, after.bubbleInstances);
|
||||
|
||||
if (changed) {
|
||||
// We have to clear the whole thing, otherwise subdomains won't be synced.
|
||||
this.federatedInstanceCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@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'];
|
||||
if (type === 'metaUpdated') {
|
||||
this.syncCache(body.before, body.after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.redisForSub.off('message', this.onMessage);
|
||||
this.federatedInstanceCache.dispose();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiMeta } from '@/models/Meta.js';
|
||||
|
|
@ -12,6 +12,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { MiInstance } from '@/models/Instance.js';
|
||||
import { diffArrays } from '@/misc/diff-arrays.js';
|
||||
import type { MetasRepository } from '@/models/_.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -26,6 +29,9 @@ export class MetaService implements OnApplicationShutdown {
|
|||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.metasRepository)
|
||||
private readonly metasRepository: MetasRepository,
|
||||
|
||||
private featuredService: FeaturedService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
|
|
@ -67,35 +73,35 @@ export class MetaService implements OnApplicationShutdown {
|
|||
public async fetch(noCache = false): Promise<MiMeta> {
|
||||
if (!noCache && this.cache) return this.cache;
|
||||
|
||||
return await this.db.transaction(async transactionalEntityManager => {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
const metas = await transactionalEntityManager.find(MiMeta, {
|
||||
order: {
|
||||
// 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
|
||||
let meta = await this.metasRepository.createQueryBuilder('meta')
|
||||
.select()
|
||||
.orderBy({
|
||||
id: 'DESC',
|
||||
})
|
||||
.limit(1)
|
||||
.getOne();
|
||||
|
||||
if (!meta) {
|
||||
await this.metasRepository.createQueryBuilder('meta')
|
||||
.insert()
|
||||
.values({
|
||||
id: 'x',
|
||||
})
|
||||
.orIgnore()
|
||||
.execute();
|
||||
|
||||
meta = await this.metasRepository.createQueryBuilder('meta')
|
||||
.select()
|
||||
.orderBy({
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
})
|
||||
.limit(1)
|
||||
.getOneOrFail();
|
||||
}
|
||||
|
||||
const meta = metas[0];
|
||||
|
||||
if (meta) {
|
||||
this.cache = meta;
|
||||
return meta;
|
||||
} else {
|
||||
// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
|
||||
const saved = await transactionalEntityManager
|
||||
.upsert(
|
||||
MiMeta,
|
||||
{
|
||||
id: 'x',
|
||||
},
|
||||
['id'],
|
||||
)
|
||||
.then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0]));
|
||||
|
||||
this.cache = saved;
|
||||
return saved;
|
||||
}
|
||||
});
|
||||
this.cache = meta;
|
||||
return meta;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -103,7 +109,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||
let before: MiMeta | undefined;
|
||||
|
||||
const updated = await this.db.transaction(async transactionalEntityManager => {
|
||||
const metas = await transactionalEntityManager.find(MiMeta, {
|
||||
const metas: (MiMeta | undefined)[] = await transactionalEntityManager.find(MiMeta, {
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
|
|
@ -126,6 +132,10 @@ export class MetaService implements OnApplicationShutdown {
|
|||
},
|
||||
});
|
||||
|
||||
// Propagate changes to blockedHosts, silencedHosts, mediaSilencedHosts, federationInstances, and bubbleInstances to the relevant instance rows
|
||||
// Do this inside the transaction to avoid potential race condition (when an instance gets registered while we're updating).
|
||||
await this.persistBlocks(transactionalEntityManager, before ?? {}, afters[0]);
|
||||
|
||||
return afters[0];
|
||||
});
|
||||
|
||||
|
|
@ -159,4 +169,49 @@ export class MetaService implements OnApplicationShutdown {
|
|||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
private async persistBlocks(tem: EntityManager, before: Partial<MiMeta>, after: Partial<MiMeta>): Promise<void> {
|
||||
await this.persistBlock(tem, before.blockedHosts, after.blockedHosts, 'isBlocked');
|
||||
await this.persistBlock(tem, before.silencedHosts, after.silencedHosts, 'isSilenced');
|
||||
await this.persistBlock(tem, before.mediaSilencedHosts, after.mediaSilencedHosts, 'isMediaSilenced');
|
||||
await this.persistBlock(tem, before.federationHosts, after.federationHosts, 'isAllowListed');
|
||||
await this.persistBlock(tem, before.bubbleInstances, after.bubbleInstances, 'isBubbled');
|
||||
}
|
||||
|
||||
private async persistBlock(tem: EntityManager, before: string[] | undefined, after: string[] | undefined, field: keyof MiInstance): Promise<void> {
|
||||
const { added, removed } = diffArrays(before, after);
|
||||
|
||||
if (removed.length > 0) {
|
||||
await this.updateInstancesByHost(tem, field, false, removed);
|
||||
}
|
||||
|
||||
if (added.length > 0) {
|
||||
await this.updateInstancesByHost(tem, field, true, added);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateInstancesByHost(tem: EntityManager, field: keyof MiInstance, value: boolean, hosts: string[]): Promise<void> {
|
||||
// Use non-array queries when possible, as they are indexed and can be much faster.
|
||||
if (hosts.length === 1) {
|
||||
const pattern = genHostPattern(hosts[0]);
|
||||
await tem
|
||||
.createQueryBuilder(MiInstance, 'instance')
|
||||
.update()
|
||||
.set({ [field]: value })
|
||||
.where('(lower(reverse("host")) || \'.\') LIKE :pattern', { pattern })
|
||||
.execute();
|
||||
} else if (hosts.length > 1) {
|
||||
const patterns = hosts.map(host => genHostPattern(host));
|
||||
await tem
|
||||
.createQueryBuilder(MiInstance, 'instance')
|
||||
.update()
|
||||
.set({ [field]: value })
|
||||
.where('(lower(reverse("host")) || \'.\') LIKE ANY (:patterns)', { patterns })
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function genHostPattern(host: string): string {
|
||||
return host.toLowerCase().split('').reverse().join('') + '.%';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,47 +243,40 @@ export class QueryService {
|
|||
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('note.renoteId IS NOT NULL');
|
||||
qb.andWhere('note.text IS NULL');
|
||||
qb.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}))
|
||||
.orWhere('note.renoteId IS NULL')
|
||||
.orWhere('note.text IS NOT NULL');
|
||||
.orWhere('note.text IS NOT NULL')
|
||||
.orWhere('note.cw IS NOT NULL')
|
||||
.orWhere('note.replyId IS NOT NULL')
|
||||
.orWhere('note.fileIds != \'{}\'')
|
||||
.orWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`);
|
||||
}));
|
||||
|
||||
q.setParameters(mutingQuery.getParameters());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public generateBlockedHostQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
|
||||
let nonBlockedHostQuery: (part: string) => string;
|
||||
if (this.meta.blockedHosts.length === 0) {
|
||||
nonBlockedHostQuery = () => '1=1';
|
||||
} else {
|
||||
nonBlockedHostQuery = (match: string) => `('.' || ${match}) NOT ILIKE ALL(select '%.' || x from (select unnest("blockedHosts") as x from "meta") t)`;
|
||||
public generateBlockedHostQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean, allowSilenced = true): void {
|
||||
function checkFor(key: 'user' | 'replyUser' | 'renoteUser') {
|
||||
q.leftJoin(`note.${key}Instance`, `${key}Instance`);
|
||||
q.andWhere(new Brackets(qb => {
|
||||
qb.orWhere(`note.${key}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.${key}Host IS NULL`) // local
|
||||
.orWhere(`${key}Instance.isBlocked = false`); // not blocked
|
||||
|
||||
if (!allowSilenced) {
|
||||
qb.orWhere(`${key}Instance.isSilenced = false`); // not silenced
|
||||
}
|
||||
|
||||
if (excludeAuthor) {
|
||||
qb.orWhere(`note.userId = note.${key}Id`); // author
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (excludeAuthor) {
|
||||
const instanceSuspension = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.userId = note.${user}Id`)
|
||||
.orWhere(`note.${user}Host IS NULL`) // local
|
||||
.orWhere(nonBlockedHostQuery(`note.${user}Host`)));
|
||||
|
||||
q
|
||||
.andWhere(instanceSuspension('replyUser'))
|
||||
.andWhere(instanceSuspension('renoteUser'));
|
||||
} else {
|
||||
const instanceSuspension = (user: string) => new Brackets(qb => qb
|
||||
.where(`note.${user}Id IS NULL`) // no corresponding user
|
||||
.orWhere(`note.${user}Host IS NULL`) // local
|
||||
.orWhere(nonBlockedHostQuery(`note.${user}Host`)));
|
||||
|
||||
q
|
||||
.andWhere(instanceSuspension('user'))
|
||||
.andWhere(instanceSuspension('replyUser'))
|
||||
.andWhere(instanceSuspension('renoteUser'));
|
||||
if (!excludeAuthor) {
|
||||
checkFor('user');
|
||||
}
|
||||
checkFor('replyUser');
|
||||
checkFor('renoteUser');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,6 +587,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
|
||||
movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
|
||||
instance: null,
|
||||
} : null,
|
||||
user2: parsed.user2 != null ? {
|
||||
...parsed.user2,
|
||||
|
|
@ -597,6 +598,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
|||
lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
|
||||
movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
|
||||
instance: null,
|
||||
} : null,
|
||||
};
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -49,22 +49,49 @@ export class UtilityService {
|
|||
return regexp.test(email);
|
||||
}
|
||||
|
||||
public isBlockedHost(host: string | null): boolean;
|
||||
public isBlockedHost(blockedHosts: string[], host: string | null): boolean;
|
||||
@bindThis
|
||||
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
|
||||
public isBlockedHost(blockedHostsOrHost: string[] | string | null, host?: string | null): boolean {
|
||||
const blockedHosts = Array.isArray(blockedHostsOrHost) ? blockedHostsOrHost : this.meta.blockedHosts;
|
||||
host = Array.isArray(blockedHostsOrHost) ? host : blockedHostsOrHost;
|
||||
|
||||
if (host == null) return false;
|
||||
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
public isSilencedHost(host: string | null): boolean;
|
||||
public isSilencedHost(silencedHosts: string[], host: string | null): boolean;
|
||||
@bindThis
|
||||
public isSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
|
||||
if (!silencedHosts || host == null) return false;
|
||||
public isSilencedHost(silencedHostsOrHost: string[] | string | null, host?: string | null): boolean {
|
||||
const silencedHosts = Array.isArray(silencedHostsOrHost) ? silencedHostsOrHost : this.meta.silencedHosts;
|
||||
host = Array.isArray(silencedHostsOrHost) ? host : silencedHostsOrHost;
|
||||
|
||||
if (host == null) return false;
|
||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
public isMediaSilencedHost(host: string | null): boolean;
|
||||
public isMediaSilencedHost(silencedHosts: string[], host: string | null): boolean;
|
||||
@bindThis
|
||||
public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
|
||||
if (!silencedHosts || host == null) return false;
|
||||
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
public isMediaSilencedHost(mediaSilencedHostsOrHost: string[] | string | null, host?: string | null): boolean {
|
||||
const mediaSilencedHosts = Array.isArray(mediaSilencedHostsOrHost) ? mediaSilencedHostsOrHost : this.meta.mediaSilencedHosts;
|
||||
host = Array.isArray(mediaSilencedHostsOrHost) ? host : mediaSilencedHostsOrHost;
|
||||
|
||||
if (host == null) return false;
|
||||
return mediaSilencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isAllowListedHost(host: string | null): boolean {
|
||||
if (host == null) return false;
|
||||
return this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isBubbledHost(host: string | null): boolean {
|
||||
if (host == null) return false;
|
||||
return this.meta.bubbleInstances.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
|||
emojis: [],
|
||||
score: 0,
|
||||
host: null,
|
||||
instance: null,
|
||||
inbox: null,
|
||||
sharedInbox: null,
|
||||
featured: null,
|
||||
|
|
@ -114,10 +115,13 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
|
|||
channelId: null,
|
||||
channel: null,
|
||||
userHost: null,
|
||||
userInstance: null,
|
||||
replyUserId: null,
|
||||
replyUserHost: null,
|
||||
replyUserInstance: null,
|
||||
renoteUserId: null,
|
||||
renoteUserHost: null,
|
||||
renoteUserInstance: null,
|
||||
updatedAt: null,
|
||||
processErrors: [],
|
||||
...override,
|
||||
|
|
@ -449,6 +453,7 @@ export class WebhookTestService {
|
|||
isAdmin: false,
|
||||
isModerator: false,
|
||||
isSystem: false,
|
||||
instance: undefined,
|
||||
...override,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,6 +398,9 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown {
|
|||
const followerscollection = await _resolver.resolveCollection(person.followers!).catch(() => { return null; });
|
||||
const followingcollection = await _resolver.resolveCollection(person.following!).catch(() => { return null; });
|
||||
|
||||
// Register the instance first, to avoid FK errors
|
||||
await this.federatedInstanceService.fetchOrRegister(host);
|
||||
|
||||
try {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
|
|
|
|||
|
|
@ -44,10 +44,6 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
}
|
||||
|
||||
protected async tickMinor(): Promise<Partial<KVs<typeof schema>>> {
|
||||
const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('instance.host')
|
||||
.where('instance.suspensionState != \'none\'');
|
||||
|
||||
const pubsubSubQuery = this.followingsRepository.createQueryBuilder('f')
|
||||
.select('f.followerHost')
|
||||
.where('f.followerHost IS NOT NULL');
|
||||
|
|
@ -64,22 +60,25 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : '(\'.\' || following.followeeHost) NOT ILIKE ALL(select \'%.\' || x from (select unnest("blockedHosts") as x from "meta") t)')
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.innerJoin('following.followeeInstance', 'followeeInstance')
|
||||
.andWhere('followeeInstance.suspensionState = \'none\'')
|
||||
.andWhere('followeeInstance.isBlocked = false')
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followerHost)')
|
||||
.where('following.followerHost IS NOT NULL')
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : '(\'.\' || following.followerHost) NOT ILIKE ALL(select \'%.\' || x from (select unnest("blockedHosts") as x from "meta") t)')
|
||||
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.innerJoin('following.followerInstance', 'followerInstance')
|
||||
.andWhere('followerInstance.isBlocked = false')
|
||||
.andWhere('followerInstance.suspensionState = \'none\'')
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : '(\'.\' || following.followeeHost) NOT ILIKE ALL(select \'%.\' || x from (select unnest("blockedHosts") as x from "meta") t)')
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.innerJoin('following.followeeInstance', 'followeeInstance')
|
||||
.andWhere('followeeInstance.isBlocked = false')
|
||||
.andWhere('followeeInstance.suspensionState = \'none\'')
|
||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||
.setParameters(pubsubSubQuery.getParameters())
|
||||
.getRawOne()
|
||||
|
|
@ -87,7 +86,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : '(\'.\' || instance.host) NOT ILIKE ALL(select \'%.\' || x from (select unnest("blockedHosts") as x from "meta") t)')
|
||||
.andWhere('instance.isBlocked = false')
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
@ -95,7 +94,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||
.andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : '(\'.\' || instance.host) NOT ILIKE ALL(select \'%.\' || x from (select unnest("blockedHosts") as x from "meta") t)')
|
||||
.andWhere('instance.isBlocked = false')
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class InstanceEntityService {
|
|||
isNotResponding: instance.isNotResponding,
|
||||
isSuspended: instance.suspensionState !== 'none',
|
||||
suspensionState: instance.suspensionState,
|
||||
isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host),
|
||||
isBlocked: instance.isBlocked,
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
openRegistrations: instance.openRegistrations,
|
||||
|
|
@ -51,8 +51,8 @@ export class InstanceEntityService {
|
|||
description: instance.description,
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host),
|
||||
isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host),
|
||||
isSilenced: instance.isSilenced,
|
||||
isMediaSilenced: instance.isMediaSilenced,
|
||||
iconUrl: instance.iconUrl,
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
|
|
|
|||
|
|
@ -609,7 +609,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
||||
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
instance: user.host ? this.federatedInstanceService.fetch(user.host).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
softwareVersion: instance.softwareVersion,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue