summaryrefslogtreecommitdiff
path: root/packages/backend/src
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-06-01 17:25:52 +0000
committerHazelnoot <acomputerdog@gmail.com>2025-06-01 17:25:52 +0000
commite1504cfb88d047acbfc5c08bcf790c0a08875ee9 (patch)
tree074df46515fd4ddd4a6bdf7b0a1af57fcb663291 /packages/backend/src
parentmerge: fix DeepLX (!1077) (diff)
parentexclude local notes from bubble timeline (diff)
downloadsharkey-e1504cfb88d047acbfc5c08bcf790c0a08875ee9.tar.gz
sharkey-e1504cfb88d047acbfc5c08bcf790c0a08875ee9.tar.bz2
sharkey-e1504cfb88d047acbfc5c08bcf790c0a08875ee9.zip
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>
Diffstat (limited to 'packages/backend/src')
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts11
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts103
-rw-r--r--packages/backend/src/core/MetaService.ts111
-rw-r--r--packages/backend/src/core/QueryService.ts55
-rw-r--r--packages/backend/src/core/ReversiService.ts2
-rw-r--r--packages/backend/src/core/UtilityService.ts39
-rw-r--r--packages/backend/src/core/WebhookTestService.ts5
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts3
-rw-r--r--packages/backend/src/core/chart/charts/federation.ts23
-rw-r--r--packages/backend/src/core/entities/InstanceEntityService.ts6
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts2
-rw-r--r--packages/backend/src/misc/cache.ts9
-rw-r--r--packages/backend/src/misc/diff-arrays.ts102
-rw-r--r--packages/backend/src/models/Following.ts21
-rw-r--r--packages/backend/src/models/Instance.ts51
-rw-r--r--packages/backend/src/models/Note.ts31
-rw-r--r--packages/backend/src/models/User.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/notes.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/clips/notes.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/notes/following.ts13
-rw-r--r--packages/backend/src/server/api/endpoints/notes/search-by-tag.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/roles/notes.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/users/reactions.ts7
25 files changed, 476 insertions, 147 deletions
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index af2723e99d..f9cf41e854 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -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);
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 3f7ed99348..34df10f0ff 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -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();
}
diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts
index 40e7439f5f..07f82dc23e 100644
--- a/packages/backend/src/core/MetaService.ts
+++ b/packages/backend/src/core/MetaService.ts
@@ -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: 'DESC',
- },
- });
+ // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する
+ let meta = await this.metasRepository.createQueryBuilder('meta')
+ .select()
+ .orderBy({
+ id: 'DESC',
+ })
+ .limit(1)
+ .getOne();
- const meta = metas[0];
+ if (!meta) {
+ await this.metasRepository.createQueryBuilder('meta')
+ .insert()
+ .values({
+ id: 'x',
+ })
+ .orIgnore()
+ .execute();
- 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]));
+ meta = await this.metasRepository.createQueryBuilder('meta')
+ .select()
+ .orderBy({
+ id: 'DESC',
+ })
+ .limit(1)
+ .getOneOrFail();
+ }
- 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('') + '.%';
}
diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts
index 50a72e8aa6..1b00f41d20 100644
--- a/packages/backend/src/core/QueryService.ts
+++ b/packages/backend/src/core/QueryService.ts
@@ -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 (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`)));
+ if (!allowSilenced) {
+ qb.orWhere(`${key}Instance.isSilenced = false`); // not silenced
+ }
- 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`)));
+ if (excludeAuthor) {
+ qb.orWhere(`note.userId = note.${key}Id`); // author
+ }
+ }));
+ }
- q
- .andWhere(instanceSuspension('user'))
- .andWhere(instanceSuspension('replyUser'))
- .andWhere(instanceSuspension('renoteUser'));
+ if (!excludeAuthor) {
+ checkFor('user');
}
+ checkFor('replyUser');
+ checkFor('renoteUser');
}
}
diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts
index 8c0a8f6cc7..e31d9e5b1a 100644
--- a/packages/backend/src/core/ReversiService.ts
+++ b/packages/backend/src/core/ReversiService.ts
@@ -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 {
diff --git a/packages/backend/src/core/UtilityService.ts b/packages/backend/src/core/UtilityService.ts
index 170afc72dc..3098367392 100644
--- a/packages/backend/src/core/UtilityService.ts
+++ b/packages/backend/src/core/UtilityService.ts
@@ -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
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 2f8cfea7f7..afd011c410 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -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,
};
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 5c6716a0b8..3ca9c93806 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -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 => {
diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts
index bf702884ca..b6db6f5454 100644
--- a/packages/backend/src/core/chart/charts/federation.ts
+++ b/packages/backend/src/core/chart/charts/federation.ts
@@ -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()
diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts
index fcc9bed3bd..332d2943a4 100644
--- a/packages/backend/src/core/entities/InstanceEntityService.ts
+++ b/packages/backend/src/core/entities/InstanceEntityService.ts
@@ -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,
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 56506a5fa4..feddb8fa94 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -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,
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index 48b8f43678..a6ab96c189 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -308,8 +308,17 @@ export class MemoryKVCache<T> {
}
}
+ /**
+ * Removes all entries from the cache, but does not dispose it.
+ */
+ @bindThis
+ public clear(): void {
+ this.cache.clear();
+ }
+
@bindThis
public dispose(): void {
+ this.clear();
clearInterval(this.gcIntervalHandle);
}
diff --git a/packages/backend/src/misc/diff-arrays.ts b/packages/backend/src/misc/diff-arrays.ts
new file mode 100644
index 0000000000..b50ca1d4f7
--- /dev/null
+++ b/packages/backend/src/misc/diff-arrays.ts
@@ -0,0 +1,102 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export interface DiffResult<T> {
+ added: T[];
+ removed: T[];
+}
+
+/**
+ * Calculates the difference between two snapshots of data.
+ * Null, undefined, and empty arrays are supported, and duplicate values are ignored.
+ * Result sets are de-duplicated, and will be empty if no data was added or removed (respectively).
+ * The inputs are treated as un-ordered, so a re-ordering of the same data will NOT be considered a change.
+ * @param dataBefore Array containing data before the change
+ * @param dataAfter Array containing data after the change
+ */
+export function diffArrays<T>(dataBefore: T[] | null | undefined, dataAfter: T[] | null | undefined): DiffResult<T> {
+ const before = dataBefore ? new Set(dataBefore) : null;
+ const after = dataAfter ? new Set(dataAfter) : null;
+
+ // data before AND after => changed
+ if (before?.size && after?.size) {
+ const added: T[] = [];
+ const removed: T[] = [];
+
+ for (const host of before) {
+ // before and NOT after => removed
+ // delete operation removes duplicates to speed up the "after" loop
+ if (!after.delete(host)) {
+ removed.push(host);
+ }
+ }
+
+ for (const host of after) {
+ // after and NOT before => added
+ if (!before.has(host)) {
+ added.push(host);
+ }
+ }
+
+ return { added, removed };
+ }
+
+ // data ONLY before => all removed
+ if (before?.size) {
+ return { added: [], removed: Array.from(before) };
+ }
+
+ // data ONLY after => all added
+ if (after?.size) {
+ return { added: Array.from(after), removed: [] };
+ }
+
+ // data NEITHER before nor after => no change
+ return { added: [], removed: [] };
+}
+
+/**
+ * Checks for any difference between two snapshots of data.
+ * Null, undefined, and empty arrays are supported, and duplicate values are ignored.
+ * The inputs are treated as un-ordered, so a re-ordering of the same data will NOT be considered a change.
+ * @param dataBefore Array containing data before the change
+ * @param dataAfter Array containing data after the change
+ */
+export function diffArraysSimple<T>(dataBefore: T[] | null | undefined, dataAfter: T[] | null | undefined): boolean {
+ const before = dataBefore ? new Set(dataBefore) : null;
+ const after = dataAfter ? new Set(dataAfter) : null;
+
+ if (before?.size && after?.size) {
+ // different size => changed
+ if (before.size !== after.size) return true;
+
+ // removed => changed
+ for (const host of before) {
+ // delete operation removes duplicates to speed up the "after" loop
+ if (!after.delete(host)) {
+ return true;
+ }
+ }
+
+ // added => changed
+ for (const host of after) {
+ if (!before.has(host)) {
+ return true;
+ }
+ }
+
+ // identical values => no change
+ return false;
+ }
+
+ // before and NOT after => change
+ if (before?.size) return true;
+
+ // after and NOT before => change
+ if (after?.size) return true;
+
+ // NEITHER before nor after => no change
+ return false;
+}
diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts
index 62cbc29f26..0aa1b13976 100644
--- a/packages/backend/src/models/Following.ts
+++ b/packages/backend/src/models/Following.ts
@@ -4,6 +4,7 @@
*/
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { MiInstance } from '@/models/Instance.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
@@ -66,6 +67,16 @@ export class MiFollowing {
})
public followerHost: string | null;
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'followerHost',
+ foreignKeyConstraintName: 'FK_following_followerHost',
+ referencedColumnName: 'host',
+ })
+ public followerInstance: MiInstance | null;
+
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]',
@@ -85,6 +96,16 @@ export class MiFollowing {
})
public followeeHost: string | null;
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'followeeHost',
+ foreignKeyConstraintName: 'FK_following_followeeHost',
+ referencedColumnName: 'host',
+ })
+ public followeeInstance: MiInstance | null;
+
@Column('varchar', {
length: 512, nullable: true,
comment: '[Denormalized]',
diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts
index c64ebb1b3b..0022e58933 100644
--- a/packages/backend/src/models/Instance.ts
+++ b/packages/backend/src/models/Instance.ts
@@ -6,6 +6,7 @@
import { Entity, PrimaryColumn, Index, Column } from 'typeorm';
import { id } from './util/id.js';
+@Index('IDX_instance_host_key', { synchronize: false })
@Entity('instance')
export class MiInstance {
@PrimaryColumn(id())
@@ -98,6 +99,56 @@ export class MiInstance {
})
public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
+ /**
+ * True if this instance is blocked from federation.
+ */
+ @Column('boolean', {
+ nullable: false,
+ default: false,
+ comment: 'True if this instance is blocked from federation.',
+ })
+ public isBlocked: boolean;
+
+ /**
+ * True if this instance is allow-listed.
+ */
+ @Column('boolean', {
+ nullable: false,
+ default: false,
+ comment: 'True if this instance is allow-listed.',
+ })
+ public isAllowListed: boolean;
+
+ /**
+ * True if this instance is part of the local bubble.
+ */
+ @Column('boolean', {
+ nullable: false,
+ default: false,
+ comment: 'True if this instance is part of the local bubble.',
+ })
+ public isBubbled: boolean;
+
+ /**
+ * True if this instance is silenced.
+ */
+ @Column('boolean', {
+ nullable: false,
+ default: false,
+ comment: 'True if this instance is silenced.',
+ })
+ public isSilenced: boolean;
+
+ /**
+ * True if this instance is media-silenced.
+ */
+ @Column('boolean', {
+ nullable: false,
+ default: false,
+ comment: 'True if this instance is media-silenced.',
+ })
+ public isMediaSilenced: boolean;
+
@Column('varchar', {
length: 64, nullable: true,
comment: 'The software of the Instance.',
diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts
index ee2098216d..fa5839b6ec 100644
--- a/packages/backend/src/models/Note.ts
+++ b/packages/backend/src/models/Note.ts
@@ -5,6 +5,7 @@
import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
import { noteVisibilities } from '@/types.js';
+import { MiInstance } from '@/models/Instance.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
@@ -222,6 +223,16 @@ export class MiNote {
})
public userHost: string | null;
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'userHost',
+ foreignKeyConstraintName: 'FK_note_userHost',
+ referencedColumnName: 'host',
+ })
+ public userInstance: MiInstance | null;
+
@Column({
...id(),
nullable: true,
@@ -235,6 +246,16 @@ export class MiNote {
})
public replyUserHost: string | null;
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'replyUserHost',
+ foreignKeyConstraintName: 'FK_note_replyUserHost',
+ referencedColumnName: 'host',
+ })
+ public replyUserInstance: MiInstance | null;
+
@Column({
...id(),
nullable: true,
@@ -247,6 +268,16 @@ export class MiNote {
comment: '[Denormalized]',
})
public renoteUserHost: string | null;
+
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'renoteUserHost',
+ foreignKeyConstraintName: 'FK_note_renoteUserHost',
+ referencedColumnName: 'host',
+ })
+ public renoteUserInstance: MiInstance | null;
//#endregion
constructor(data: Partial<MiNote>) {
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 46f8e84a94..55b8f4f4f0 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn, ManyToOne } from 'typeorm';
import { type UserUnsignedFetchOption, userUnsignedFetchOptions } from '@/const.js';
+import { MiInstance } from '@/models/Instance.js';
import { id } from './util/id.js';
import { MiDriveFile } from './DriveFile.js';
@@ -292,6 +293,16 @@ export class MiUser {
})
public host: string | null;
+ @ManyToOne(() => MiInstance, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn({
+ name: 'host',
+ foreignKeyConstraintName: 'FK_user_host',
+ referencedColumnName: 'host',
+ })
+ public instance: MiInstance | null;
+
@Column('varchar', {
length: 512, nullable: true,
comment: 'The inbox URL of the User. It will be null if the origin of the user is local.',
diff --git a/packages/backend/src/server/api/endpoints/antennas/notes.ts b/packages/backend/src/server/api/endpoints/antennas/notes.ts
index b90ba6aa0d..7e79f0dccc 100644
--- a/packages/backend/src/server/api/endpoints/antennas/notes.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/notes.ts
@@ -121,6 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 6336f43e9f..99ae1c2211 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -138,9 +138,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
if (ps.withRenotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/clips/notes.ts b/packages/backend/src/server/api/endpoints/clips/notes.ts
index 59513e530d..4758dbad00 100644
--- a/packages/backend/src/server/api/endpoints/clips/notes.ts
+++ b/packages/backend/src/server/api/endpoints/clips/notes.ts
@@ -92,10 +92,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateVisibilityQuery(query, me);
if (me) {
- this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
const notes = await query
diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
index df030d90aa..7c375cb0f5 100644
--- a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
@@ -85,7 +85,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
- .andWhere('note.userHost IN (:...hosts)', { hosts: this.serverSettings.bubbleInstances })
+ .andWhere('note.userHost IS NULL')
+ .andWhere('userInstance.isBubbled = true') // This comes from generateVisibilityQuery below
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts
index 5f6ee9f903..088b172ba4 100644
--- a/packages/backend/src/server/api/endpoints/notes/following.ts
+++ b/packages/backend/src/server/api/endpoints/notes/following.ts
@@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
+import { IsNull, ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { SkLatestNote, MiFollowing } from '@/models/_.js';
import type { NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -130,7 +130,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
- .leftJoinAndSelect('note.channel', 'channel')
+
+ // Exclude channel notes
+ .andWhere({ channelId: IsNull() })
;
// Limit to files, if requested
@@ -145,11 +147,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Hide blocked users / instances
query.andWhere('"user"."isSuspended" = false');
- query.andWhere('("replyUser" IS NULL OR "replyUser"."isSuspended" = false)');
- query.andWhere('("renoteUser" IS NULL OR "renoteUser"."isSuspended" = false)');
this.queryService.generateBlockedHostQueryForNote(query);
- // Respect blocks and mutes
+ // Respect blocks, mutes, and privacy
+ this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
@@ -161,7 +162,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Query and return the next page
const notes = await query.getMany();
- return await this.noteEntityService.packMany(notes, me);
+ return await this.noteEntityService.packMany(notes, me, { skipHide: 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 91874a8195..5c1ab0fb78 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
@@ -96,10 +96,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!this.serverSettings.enableBotTrending) query.andWhere('user.isBot = FALSE');
- this.queryService.generateVisibilityQuery(query, me);
- this.queryService.generateBlockedHostQueryForNote(query);
+ this.queryService.generateBlockedHostQueryForNote(query, undefined, false);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
+ if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
const followings = me ? await this.cacheService.userFollowingsCache.fetch(me.id) : {};
@@ -160,7 +160,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (note.user?.isSuspended) return false;
if (note.userHost) {
if (!this.utilityService.isFederationAllowedHost(note.userHost)) return false;
- if (this.utilityService.isSilencedHost(this.serverSettings.silencedHosts, note.userHost)) return false;
}
return true;
});
diff --git a/packages/backend/src/server/api/endpoints/roles/notes.ts b/packages/backend/src/server/api/endpoints/roles/notes.ts
index d1c2e4b686..536384a381 100644
--- a/packages/backend/src/server/api/endpoints/roles/notes.ts
+++ b/packages/backend/src/server/api/endpoints/roles/notes.ts
@@ -107,10 +107,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
- this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index 56f59bd285..553787ad58 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -105,10 +105,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('reaction.userId = :userId', { userId: ps.userId })
- .leftJoinAndSelect('reaction.note', 'note');
+ .innerJoinAndSelect('reaction.note', 'note');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
+ if (me) {
+ this.queryService.generateMutedUserQueryForNotes(query, me);
+ this.queryService.generateBlockedUserQueryForNotes(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
+ }
const reactions = (await query
.limit(ps.limit)