refactor(api): tidy the api and shared packages (#2733)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 17:37:48 +02:00
1 parent 580401d2dc
commit 5afbf67a70
333 files changed
+10180 -7910

No files matched your search

@@ -75,12 +75,27 @@ const DEFAULT_VOICE_CATEGORY_NAME = 'Voice Channels';
const DEFAULT_TEXT_CHANNEL_NAME = 'general';
const DEFAULT_VOICE_CHANNEL_NAME = 'General';
interface PreparedGuildAssets {
icon: PreparedAssetUpload | null;
banner: PreparedAssetUpload | null;
splash: PreparedAssetUpload | null;
embed_splash: PreparedAssetUpload | null;
}
const GUILD_IMAGE_FIELDS = [
{field: 'icon', hash: 'icon_hash', dimensions: null, gate: null},
{
field: 'banner',
hash: 'banner_hash',
dimensions: {width: 'banner_width', height: 'banner_height'},
gate: {feature: GuildFeatures.BANNER, error: ValidationErrorCodes.GUILD_BANNER_REQUIRES_FEATURE},
},
{
field: 'splash',
hash: 'splash_hash',
dimensions: {width: 'splash_width', height: 'splash_height'},
gate: {feature: GuildFeatures.INVITE_SPLASH, error: ValidationErrorCodes.INVITE_SPLASH_REQUIRES_FEATURE},
},
{
field: 'embed_splash',
hash: 'embed_splash_hash',
dimensions: {width: 'embed_splash_width', height: 'embed_splash_height'},
gate: {feature: GuildFeatures.INVITE_SPLASH, error: ValidationErrorCodes.EMBED_SPLASH_REQUIRES_FEATURE},
},
] as const;
interface TemplateGuildSettings {
verificationLevel: number;
@@ -451,130 +466,10 @@ export class GuildOperationsService {
}
const updatedFeatures = this.computeUpdatedFeatures(previousFeatures, data.features);
const featuresChanged = !setsEqual(previousFeatures, updatedFeatures);
const preparedAssets: PreparedGuildAssets = {icon: null, banner: null, splash: null, embed_splash: null};
const patch: Partial<GuildRow> = {};
if (data.name !== undefined) {
patch.name = data.name;
}
if (data.icon !== undefined) {
preparedAssets.icon = await this.entityAssetService.prepareAssetUpload({
assetType: 'icon',
entityType: 'guild',
entityId: guildId,
previousHash: currentGuild.iconHash,
base64Image: data.icon,
errorPath: 'icon',
});
patch.icon_hash = preparedAssets.icon.newHash;
}
if (data.banner !== undefined) {
if (data.banner && !currentGuild.features.has(GuildFeatures.BANNER)) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode('banner', ValidationErrorCodes.GUILD_BANNER_REQUIRES_FEATURE);
}
if (data.banner === null) {
patch.banner_hash = null;
patch.banner_width = null;
patch.banner_height = null;
} else {
try {
preparedAssets.banner = await this.entityAssetService.prepareAssetUpload({
assetType: 'banner',
entityType: 'guild',
entityId: guildId,
previousHash: currentGuild.bannerHash,
base64Image: data.banner,
errorPath: 'banner',
});
if (preparedAssets.banner.isAnimated && !currentGuild.features.has(GuildFeatures.ANIMATED_BANNER)) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode('banner', ValidationErrorCodes.ANIMATED_GUILD_BANNER_REQUIRES_FEATURE);
}
patch.banner_hash = preparedAssets.banner.newHash;
patch.banner_height =
preparedAssets.banner.newHash === currentGuild.bannerHash && currentGuild.bannerHeight != null
? currentGuild.bannerHeight
: (preparedAssets.banner.height ?? null);
patch.banner_width =
preparedAssets.banner.newHash === currentGuild.bannerHash && currentGuild.bannerWidth != null
? currentGuild.bannerWidth
: (preparedAssets.banner.width ?? null);
} catch (error) {
await this.rollbackPreparedAssets(preparedAssets);
throw error;
}
}
}
if (data.splash !== undefined) {
if (data.splash && !currentGuild.features.has(GuildFeatures.INVITE_SPLASH)) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode('splash', ValidationErrorCodes.INVITE_SPLASH_REQUIRES_FEATURE);
}
if (data.splash === null) {
patch.splash_hash = null;
patch.splash_width = null;
patch.splash_height = null;
} else {
try {
preparedAssets.splash = await this.entityAssetService.prepareAssetUpload({
assetType: 'splash',
entityType: 'guild',
entityId: guildId,
previousHash: currentGuild.splashHash,
base64Image: data.splash,
errorPath: 'splash',
});
patch.splash_hash = preparedAssets.splash.newHash;
patch.splash_height =
preparedAssets.splash.newHash === currentGuild.splashHash && currentGuild.splashHeight != null
? currentGuild.splashHeight
: (preparedAssets.splash.height ?? null);
patch.splash_width =
preparedAssets.splash.newHash === currentGuild.splashHash && currentGuild.splashWidth != null
? currentGuild.splashWidth
: (preparedAssets.splash.width ?? null);
} catch (error) {
await this.rollbackPreparedAssets(preparedAssets);
throw error;
}
}
}
if (data.embed_splash !== undefined) {
if (data.embed_splash && !currentGuild.features.has(GuildFeatures.INVITE_SPLASH)) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode('embed_splash', ValidationErrorCodes.EMBED_SPLASH_REQUIRES_FEATURE);
}
if (data.embed_splash === null) {
patch.embed_splash_hash = null;
patch.embed_splash_width = null;
patch.embed_splash_height = null;
} else {
try {
preparedAssets.embed_splash = await this.entityAssetService.prepareAssetUpload({
assetType: 'embed_splash',
entityType: 'guild',
entityId: guildId,
previousHash: currentGuild.embedSplashHash,
base64Image: data.embed_splash,
errorPath: 'embed_splash',
});
patch.embed_splash_hash = preparedAssets.embed_splash.newHash;
patch.embed_splash_height =
preparedAssets.embed_splash.newHash === currentGuild.embedSplashHash &&
currentGuild.embedSplashHeight != null
? currentGuild.embedSplashHeight
: (preparedAssets.embed_splash.height ?? null);
patch.embed_splash_width =
preparedAssets.embed_splash.newHash === currentGuild.embedSplashHash &&
currentGuild.embedSplashWidth != null
? currentGuild.embedSplashWidth
: (preparedAssets.embed_splash.width ?? null);
} catch (error) {
await this.rollbackPreparedAssets(preparedAssets);
throw error;
}
}
}
if (data.splash_card_alignment !== undefined) {
patch.splash_card_alignment = data.splash_card_alignment;
}
@@ -663,14 +558,12 @@ export class GuildOperationsService {
const cutoffDate = new Date(data.message_history_cutoff);
const guildCreationTimestamp = extractTimestamp(guildId.toString());
if (cutoffDate.getTime() < guildCreationTimestamp) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode(
'message_history_cutoff',
ValidationErrorCodes.MESSAGE_HISTORY_CUTOFF_BEFORE_GUILD_CREATION,
);
}
if (cutoffDate.getTime() > Date.now()) {
await this.rollbackPreparedAssets(preparedAssets);
throw InputValidationError.fromCode(
'message_history_cutoff',
ValidationErrorCodes.MESSAGE_HISTORY_CUTOFF_IN_FUTURE,
@@ -682,6 +575,7 @@ export class GuildOperationsService {
if (featuresChanged) {
patch.features = updatedFeatures;
}
const preparedAssets = await this.prepareGuildAssetUpdates(currentGuild, data, patch);
let updatedGuild: Guild;
if (Object.keys(patch).length === 0) {
updatedGuild = currentGuild;
@@ -689,13 +583,12 @@ export class GuildOperationsService {
try {
updatedGuild = await this.guildRepository.upsertPartial(guildId, patch, currentGuild.toRow());
} catch (error) {
await this.rollbackPreparedAssets(preparedAssets);
Logger.error({error, guildId}, 'Guild update failed, rolled back asset uploads');
Logger.error({error, guildId}, 'Guild update failed with unknown commit status; retaining uploaded assets');
throw error;
}
}
try {
await this.commitPreparedAssets(preparedAssets);
await this.entityAssetService.commitAssetChanges(preparedAssets);
} catch (error) {
Logger.error({error, guildId}, 'Failed to commit asset changes after successful guild update');
}
@@ -730,12 +623,6 @@ export class GuildOperationsService {
changes: auditLogChanges,
});
}
if (data.name !== undefined && currentGuild.name !== updatedGuild.name) {
}
if (data.icon !== undefined && currentGuild.iconHash !== updatedGuild.iconHash) {
}
if (data.banner !== undefined && currentGuild.bannerHash !== updatedGuild.bannerHash) {
}
return {
guild: mapGuildToGuildResponse(updatedGuild),
previousFeatures,
@@ -769,40 +656,48 @@ export class GuildOperationsService {
return next;
}
private async rollbackPreparedAssets(assets: PreparedGuildAssets): Promise<void> {
const rollbackPromises: Array<Promise<void>> = [];
if (assets.icon) {
rollbackPromises.push(this.entityAssetService.rollbackAssetUpload(assets.icon));
private async prepareGuildAssetUpdates(
guild: Guild,
data: GuildUpdateRequest,
patch: Partial<GuildRow>,
): Promise<Array<PreparedAssetUpload>> {
const previous = guild.toRow();
const preparedAssets: Array<PreparedAssetUpload> = [];
try {
for (const {field, hash, dimensions, gate} of GUILD_IMAGE_FIELDS) {
const image = data[field];
if (image === undefined) continue;
if (image && gate && !guild.features.has(gate.feature)) {
throw InputValidationError.fromCode(field, gate.error);
}
const prepared = await this.entityAssetService.prepareAssetUpload({
assetType: field,
entityType: 'guild',
entityId: guild.id,
previousHash: previous[hash],
base64Image: image,
errorPath: field,
});
preparedAssets.push(prepared);
if (field === 'banner' && prepared.isAnimated && !guild.features.has(GuildFeatures.ANIMATED_BANNER)) {
throw InputValidationError.fromCode(field, ValidationErrorCodes.ANIMATED_GUILD_BANNER_REQUIRES_FEATURE);
}
patch[hash] = prepared.newHash;
if (!dimensions) continue;
if (image === null) {
patch[dimensions.width] = null;
patch[dimensions.height] = null;
continue;
}
const sameHash = prepared.newHash === previous[hash];
patch[dimensions.width] = (sameHash ? previous[dimensions.width] : null) ?? prepared.width ?? null;
patch[dimensions.height] = (sameHash ? previous[dimensions.height] : null) ?? prepared.height ?? null;
}
return preparedAssets;
} catch (error) {
await Promise.all(preparedAssets.map((prepared) => this.entityAssetService.rollbackAssetUpload(prepared)));
throw error;
}
if (assets.banner) {
rollbackPromises.push(this.entityAssetService.rollbackAssetUpload(assets.banner));
}
if (assets.splash) {
rollbackPromises.push(this.entityAssetService.rollbackAssetUpload(assets.splash));
}
if (assets.embed_splash) {
rollbackPromises.push(this.entityAssetService.rollbackAssetUpload(assets.embed_splash));
}
await Promise.all(rollbackPromises);
}
private async commitPreparedAssets(assets: PreparedGuildAssets): Promise<void> {
const commitPromises: Array<Promise<void>> = [];
if (assets.icon) {
commitPromises.push(this.entityAssetService.commitAssetChange({prepared: assets.icon, deferDeletion: true}));
}
if (assets.banner) {
commitPromises.push(this.entityAssetService.commitAssetChange({prepared: assets.banner, deferDeletion: true}));
}
if (assets.splash) {
commitPromises.push(this.entityAssetService.commitAssetChange({prepared: assets.splash, deferDeletion: true}));
}
if (assets.embed_splash) {
commitPromises.push(
this.entityAssetService.commitAssetChange({prepared: assets.embed_splash, deferDeletion: true}),
);
}
await Promise.all(commitPromises);
}
async deleteGuild(
@@ -359,10 +359,13 @@ export class GuildMemberOperationsService {
try {
updatedMember = await this.guildRepository.upsertMember(updatedMemberData);
} catch (error) {
await this.rollbackPreparedAssets(preparedAssets);
Logger.error(
{error, guildId, userId: targetId},
'Guild member update failed with unknown commit status; retaining uploaded assets',
);
throw error;
}
await this.commitPreparedAssets(preparedAssets);
await this.entityAssetService.commitAssetChanges([preparedAssets.avatar, preparedAssets.banner]);
if (shouldRemoveTemporaryStatus) {
await this.gatewayService.removeTemporaryGuild({userId: targetId, guildId});
}
@@ -1034,17 +1037,6 @@ export class GuildMemberOperationsService {
await Promise.all(rollbackPromises);
}
private async commitPreparedAssets(preparedAssets: PreparedMemberAssets): Promise<void> {
const commitPromises: Array<Promise<void>> = [];
if (preparedAssets.avatar) {
commitPromises.push(this.entityAssetService.commitAssetChange({prepared: preparedAssets.avatar}));
}
if (preparedAssets.banner) {
commitPromises.push(this.entityAssetService.commitAssetChange({prepared: preparedAssets.banner}));
}
await Promise.all(commitPromises);
}
private async snapshotMembershipMetadata(member: GuildMember): Promise<void> {
const existingMetadata = await this.guildRepository.getMembershipMetadata(member.guildId, member.userId);
await this.guildRepository.upsertMembershipMetadata(
@@ -6,29 +6,43 @@ import type {GuildMember} from '../../../models/GuildMember';
import type {User} from '../../../models/User';
import {getGuildMemberSearchService} from '../../../SearchFactory';
import type {IGuildMemberSearchService} from '../../../search/IGuildMemberSearchService';
import type {IGuildRepositoryAggregate} from '../../repositories/IGuildRepositoryAggregate';
interface GuildMemberSearchIndexOptions {
includeDefault?: boolean;
}
function getGuildMemberIndexServices(options: GuildMemberSearchIndexOptions = {}): Array<IGuildMemberSearchService> {
const services: Array<IGuildMemberSearchService> = [];
function getGuildMemberIndexService(options: GuildMemberSearchIndexOptions = {}): IGuildMemberSearchService | null {
const includeDefault = options.includeDefault ?? true;
const defaultService = getGuildMemberSearchService();
if (includeDefault && defaultService) {
services.push(defaultService);
}
return services;
const service = getGuildMemberSearchService();
return includeDefault ? service : null;
}
export class GuildMemberSearchIndexService {
async updateUserMembers(
user: User,
guildIds: Array<GuildID>,
guildRepository: IGuildRepositoryAggregate,
): Promise<void> {
if (guildIds.length === 0) return;
const guilds = await guildRepository.listGuilds(guildIds);
const indexedGuilds = guilds.filter((guild) => guild.membersIndexedAt != null);
if (indexedGuilds.length === 0) return;
const members = await Promise.all(indexedGuilds.map((guild) => guildRepository.getMember(guild.id, user.id)));
for (const member of members) {
if (member) {
void this.updateMember(member, user);
}
}
}
async indexMember(member: GuildMember, user: User, options?: GuildMemberSearchIndexOptions): Promise<void> {
try {
const searchServices = getGuildMemberIndexServices(options);
if (searchServices.length === 0) {
const searchService = getGuildMemberIndexService(options);
if (!searchService) {
return;
}
await Promise.all(searchServices.map((searchService) => searchService.indexMember(member, user)));
await searchService.indexMember(member, user);
} catch (error) {
Logger.error(
{
@@ -43,11 +57,11 @@ export class GuildMemberSearchIndexService {
async updateMember(member: GuildMember, user: User, options?: GuildMemberSearchIndexOptions): Promise<void> {
try {
const searchServices = getGuildMemberIndexServices(options);
if (searchServices.length === 0) {
const searchService = getGuildMemberIndexService(options);
if (!searchService) {
return;
}
await Promise.all(searchServices.map((searchService) => searchService.updateMember(member, user)));
await searchService.updateMember(member, user);
} catch (error) {
Logger.error(
{
@@ -62,11 +76,11 @@ export class GuildMemberSearchIndexService {
async deleteMember(guildId: GuildID, userId: UserID, options?: GuildMemberSearchIndexOptions): Promise<void> {
try {
const searchServices = getGuildMemberIndexServices(options);
if (searchServices.length === 0) {
const searchService = getGuildMemberIndexService(options);
if (!searchService) {
return;
}
await Promise.all(searchServices.map((searchService) => searchService.deleteMember(guildId, userId)));
await searchService.deleteMember(guildId, userId);
} catch (error) {
Logger.error(
{