fix(api): delete guilds in a background job (#3417)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-12 14:11:06 +02:00
1 parent 85d9ef7386
commit 0ba15ee46a
19 files changed
+268 -32

No files matched your search

@@ -48,6 +48,7 @@ import {addGuildToUncategorizedFolder, removeGuildFromUserFolders} from '@app/ap
import {serializeGuildForAudit} from '@app/api/utils/AuditSerializationUtils';
import {mapWithConcurrency} from '@app/api/utils/ConcurrencyUtils';
import type {WebhookRepository} from '@app/api/webhook/WebhookRepository';
import type {WorkerTaskName} from '@app/api/worker/WorkerLaneConfig';
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {ALL_PERMISSIONS, ChannelTypes, DEFAULT_PERMISSIONS, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {DiscoveryApplicationStatus} from '@fluxer/constants/src/DiscoveryConstants';
@@ -91,6 +92,7 @@ import type {
TemplateSerializedGuild,
} from '@fluxer/schema/src/domains/guild/GuildTemplateSchemas';
import {extractTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils';
import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService';
const GUILD_IMAGE_FIELDS = [
{field: 'icon', hash: 'icon_hash', dimensions: null, gate: null},
@@ -183,6 +185,8 @@ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
const TEMPLATE_AFK_TIMEOUT_MIN_SECONDS = 60;
const THREAD_PURGE_CONCURRENCY = 8;
const GUILD_DELETION_MEMBER_CONCURRENCY = 25;
const GUILD_DELETION_PENDING_FEATURES = [GuildFeatures.UNAVAILABLE_FOR_EVERYONE, GuildFeatures.UNAVAILABLE_HIDDEN];
const TEMPLATE_AFK_TIMEOUT_MAX_SECONDS = 3600;
const THE_OTHER_PLATFORM_GUILD_STAGE_VOICE_CHANNEL_TYPE = 13;
@@ -204,6 +208,7 @@ export class GuildOperationsService {
private readonly helpers: GuildDataHelpers,
private readonly limitConfigService: LimitConfigService,
private readonly discoveryRepository: GuildDiscoveryRepository,
private readonly workerService: IWorkerService<WorkerTaskName>,
) {}
async getGuild(params: {userId: UserID; guildId: GuildID}): Promise<GuildResponse> {
@@ -748,18 +753,43 @@ export class GuildOperationsService {
if (!guildData || guildData.owner_id !== user.id.toString()) {
throw new MissingPermissionsError();
}
await this.performGuildDeletion(guildId, 'source_deleted');
await this.scheduleGuildDeletion(guildId, 'source_deleted');
}
async deleteGuildById(guildId: GuildID): Promise<void> {
await this.performGuildDeletion(guildId, 'purge');
await this.scheduleGuildDeletion(guildId, 'purge');
}
private async performGuildDeletion(guildId: GuildID, copyMode: ChannelFollowerRemovalCopyMode): Promise<void> {
private async scheduleGuildDeletion(guildId: GuildID, copyMode: ChannelFollowerRemovalCopyMode): Promise<void> {
const guild = await this.guildRepository.findUnique(guildId);
if (!guild) {
throw new UnknownGuildError();
}
const features = new Set(guild.features);
for (const feature of GUILD_DELETION_PENDING_FEATURES) {
features.add(feature);
}
await this.guildRepository.upsertPartial(guildId, {features}, guild.toRow());
await this.gatewayService.reloadGuild(guildId).catch((error) => {
Logger.warn({guildId, error}, 'Failed to reload guild after scheduling deletion');
});
await this.gatewayService.dispatchGuild({
guildId,
event: 'GUILD_DELETE',
data: {id: guildId.toString()},
});
await this.workerService.addJob(
'guildProcessDeletion',
{guildId: guildId.toString(), copyMode},
{jobKey: `guild-deletion-${guildId}`},
);
}
async purgeGuild(guildId: GuildID, copyMode: ChannelFollowerRemovalCopyMode): Promise<void> {
const guild = await this.guildRepository.findUnique(guildId);
if (!guild) {
return;
}
const channels = await this.channelRepository.channelData.listGuildChannels(guildId, 'complete');
for (const channel of channels) {
await scheduleDeletedChannelFollowerRemoval({channel, crossposts: this.channelRepository.crossposts, copyMode});
@@ -770,25 +800,19 @@ export class GuildOperationsService {
event: 'GUILD_DELETE',
data: {id: guildId.toString()},
});
await Promise.all(
members.map(async (member) => {
await this.gatewayService.leaveGuild({userId: member.userId, guildId});
}),
);
await Promise.all(members.map((member) => this.userRepository.deleteGuildSettings(member.userId, guildId)));
await Promise.all(
members.map(async (member) => {
const user = await this.userRepository.findUnique(member.userId);
if (user && !user.isBot) {
await removeGuildFromUserFolders({
userId: member.userId,
guildId,
userRepository: this.userRepository,
gatewayService: this.gatewayService,
});
}
}),
);
await mapWithConcurrency(members, GUILD_DELETION_MEMBER_CONCURRENCY, async (member) => {
await this.gatewayService.leaveGuild({userId: member.userId, guildId});
await this.userRepository.deleteGuildSettings(member.userId, guildId);
const user = await this.userRepository.findUnique(member.userId);
if (user && !user.isBot) {
await removeGuildFromUserFolders({
userId: member.userId,
guildId,
userRepository: this.userRepository,
gatewayService: this.gatewayService,
});
}
});
const invites = await this.inviteRepository.listGuildInvites(guildId);
await Promise.all(invites.map((invite) => this.inviteRepository.delete(invite.code)));
const webhooks = await this.webhookRepository.listByGuild(guildId);