mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
refactor(api): tidy the api and shared packages (#2733)
This commit is contained in:
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(
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user