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

+65 -128
View File
@@ -15,6 +15,7 @@ import {InternalServerError} from '@fluxer/errors/src/domains/core/InternalServe
import {BotUserNotFoundError} from '@fluxer/errors/src/domains/oauth/BotUserNotFoundError';
import {UnclaimedAccountCannotCreateApplicationsError} from '@fluxer/errors/src/domains/oauth/UnclaimedAccountCannotCreateApplicationsError';
import {UnknownApplicationError} from '@fluxer/errors/src/domains/oauth/UnknownApplicationError';
import type {BotProfileUpdateRequest} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
import type {ApiContext} from '../ApiContext';
import type {ApplicationID, UserID} from '../BrandedTypes';
import {applicationIdToUserId} from '../BrandedTypes';
@@ -31,6 +32,7 @@ import type {Application} from '../models/Application';
import type {User} from '../models/User';
import {enforceFluxerTagChangeRateLimit} from '../user/FluxerTagChangeRateLimit';
import {hasPartialUserFieldsChanged, mapUserToPrivateResponse} from '../user/UserMappers';
import {runAllInOrder} from '../utils/ConcurrencyUtils';
import {hashPassword} from '../utils/PasswordUtils';
import {generateRandomUsername} from '../utils/UsernameGenerator';
import {deriveUsernameFromDisplayName} from '../utils/UsernameSuggestionUtils';
@@ -48,6 +50,11 @@ interface ApplicationServiceDeps {
userCacheService: UserCacheService;
}
const BOT_IMAGE_FIELDS = [
{field: 'avatar', hash: 'avatar_hash', previousHash: 'avatarHash'},
{field: 'banner', hash: 'banner_hash', previousHash: 'bannerHash'},
] as const;
export class ApplicationNotOwnedError extends ForbiddenError {
constructor() {
super({code: APIErrorCodes.APPLICATION_NOT_OWNED});
@@ -435,23 +442,16 @@ export class ApplicationService {
async updateBotProfile(
userId: UserID,
applicationId: ApplicationID,
args: {
username?: string;
discriminator?: number;
avatar?: string | null;
banner?: string | null;
bio?: string | null;
botFlags?: number;
},
args: BotProfileUpdateRequest,
): Promise<{
user: User;
application: Application;
}> {
const application = await this.verifyOwnership(userId, applicationId);
if (!application.hasBotUser()) {
const botUserId = application.getBotUserId();
if (botUserId === null) {
throw new BotUserNotFoundError();
}
const botUserId = application.getBotUserId()!;
const botUser = await this.apiContext.services.users.findUnique(botUserId);
if (!botUser) {
throw new BotUserNotFoundError();
@@ -493,77 +493,62 @@ export class ApplicationService {
}
}
updates.global_name = null;
const assetPrep = await this.prepareBotAssets({
botUser,
botUserId,
avatar: args.avatar,
banner: args.banner,
});
if (assetPrep.avatarHash !== undefined) {
updates.avatar_hash = assetPrep.avatarHash;
}
if (assetPrep.bannerHash !== undefined) {
updates.banner_hash = assetPrep.bannerHash;
}
if (args.bio !== undefined) {
if (args.bio && profileSubstringBlocklistCache.containsBannedSubstring('bio', args.bio)) {
throw new ContentBlockedError();
}
updates.bio = args.bio;
}
if (args.botFlags !== undefined) {
const friendlyFlag = UserFlags.FRIENDLY_BOT;
const manualApprovalFlag = UserFlags.FRIENDLY_BOT_MANUAL_APPROVAL;
const desiredFriendly = (BigInt(args.botFlags) & friendlyFlag) === friendlyFlag;
const desiredManualApproval = (BigInt(args.botFlags) & manualApprovalFlag) === manualApprovalFlag;
const currentlyFriendly = (botUser.flags & friendlyFlag) === friendlyFlag;
const currentlyManualApproval = (botUser.flags & manualApprovalFlag) === manualApprovalFlag;
let updatedFlags = botUser.flags;
if (desiredFriendly && !currentlyFriendly) {
updatedFlags |= friendlyFlag;
} else if (!desiredFriendly && currentlyFriendly) {
updatedFlags &= ~friendlyFlag;
}
if (desiredManualApproval && !currentlyManualApproval) {
updatedFlags |= manualApprovalFlag;
} else if (!desiredManualApproval && currentlyManualApproval) {
updatedFlags &= ~manualApprovalFlag;
}
if (args.bot_flags !== undefined) {
const mask = UserFlags.FRIENDLY_BOT | UserFlags.FRIENDLY_BOT_MANUAL_APPROVAL;
const updatedFlags = (botUser.flags & ~mask) | (BigInt(args.bot_flags) & mask);
if (updatedFlags !== botUser.flags) {
updates.flags = updatedFlags;
}
}
let updatedUser: User | null;
const preparedAssets = await this.prepareBotAssets(botUser, args, updates);
let updatedUser: User;
try {
updatedUser = await this.apiContext.services.users.patchUpsert(botUserId, updates, botUser.toRow());
} catch (err) {
await this.rollbackBotAssets(assetPrep);
Logger.error(
{error: err, applicationId: applicationId.toString(), botUserId: botUserId.toString()},
'Bot profile update failed with unknown commit status; retaining uploaded assets',
);
throw err;
}
if (!updatedUser) {
await this.rollbackBotAssets(assetPrep);
throw new BotUserNotFoundError();
}
try {
await this.apiContext.services.contactChangeLog.recordDiff({
oldUser: botUser,
newUser: updatedUser,
reason: 'user_requested',
actorUserId: userId,
});
await this.commitBotAssets(assetPrep);
await runAllInOrder(
[
() =>
this.apiContext.services.contactChangeLog.recordDiff({
oldUser: botUser,
newUser: updatedUser,
reason: 'user_requested',
actorUserId: userId,
}),
() => this.deps.entityAssetService.commitAssetChanges(preparedAssets),
() =>
this.apiContext.services.gateway.dispatchPresence({
userId: updatedUser.id,
event: 'USER_UPDATE',
data: mapUserToPrivateResponse(updatedUser),
}),
async () => {
if (hasPartialUserFieldsChanged(botUser, updatedUser)) {
await this.deps.userCacheService.setUserPartialResponseFromUser(updatedUser);
}
},
],
'Failed to finalize bot profile',
);
} catch (err) {
await this.rollbackBotAssets(assetPrep);
Logger.error(
{error: err, applicationId: applicationId.toString(), botUserId: botUserId.toString()},
'Failed to finalize bot profile after successful DB update',
);
throw err;
}
await this.apiContext.services.gateway.dispatchPresence({
userId: updatedUser.id,
event: 'USER_UPDATE',
data: mapUserToPrivateResponse(updatedUser),
});
if (hasPartialUserFieldsChanged(botUser, updatedUser)) {
await this.deps.userCacheService.setUserPartialResponseFromUser(updatedUser);
}
Logger.info(
{applicationId: applicationId.toString(), botUserId: botUserId.toString()},
'Successfully updated bot profile',
@@ -574,74 +559,26 @@ export class ApplicationService {
};
}
private async prepareBotAssets(params: {
botUser: User;
botUserId: UserID;
avatar?: string | null;
banner?: string | null;
}): Promise<{
avatarUpload: PreparedAssetUpload | null;
bannerUpload: PreparedAssetUpload | null;
avatarHash: string | null | undefined;
bannerHash: string | null | undefined;
}> {
const {botUser, botUserId, avatar, banner} = params;
let avatarUpload: PreparedAssetUpload | null = null;
let bannerUpload: PreparedAssetUpload | null = null;
let avatarHash: string | null | undefined;
let bannerHash: string | null | undefined;
if (avatar !== undefined) {
avatarUpload = await this.deps.entityAssetService.prepareAssetUpload({
assetType: 'avatar',
private async prepareBotAssets(
user: User,
data: BotProfileUpdateRequest,
updates: Partial<UserRow>,
): Promise<Array<PreparedAssetUpload>> {
const preparedAssets: Array<PreparedAssetUpload> = [];
for (const {field, hash, previousHash} of BOT_IMAGE_FIELDS) {
const image = data[field];
if (image === undefined) continue;
const prepared = await this.deps.entityAssetService.prepareAssetUpload({
assetType: field,
entityType: 'user',
entityId: botUserId,
previousHash: botUser.avatarHash,
base64Image: avatar,
errorPath: 'avatar',
entityId: user.id,
previousHash: user[previousHash],
base64Image: image,
errorPath: field,
});
avatarHash = avatarUpload.newHash;
if (avatarUpload.newHash === botUser.avatarHash) {
avatarUpload = null;
}
}
if (banner !== undefined) {
bannerUpload = await this.deps.entityAssetService.prepareAssetUpload({
assetType: 'banner',
entityType: 'user',
entityId: botUserId,
previousHash: botUser.bannerHash,
base64Image: banner,
errorPath: 'banner',
});
bannerHash = bannerUpload.newHash;
if (bannerUpload.newHash === botUser.bannerHash) {
bannerUpload = null;
}
}
return {avatarUpload, bannerUpload, avatarHash, bannerHash};
}
private async commitBotAssets(assetPrep: {
avatarUpload: PreparedAssetUpload | null;
bannerUpload: PreparedAssetUpload | null;
}) {
if (assetPrep.avatarUpload) {
await this.deps.entityAssetService.commitAssetChange({prepared: assetPrep.avatarUpload, deferDeletion: true});
}
if (assetPrep.bannerUpload) {
await this.deps.entityAssetService.commitAssetChange({prepared: assetPrep.bannerUpload, deferDeletion: true});
}
}
private async rollbackBotAssets(assetPrep: {
avatarUpload: PreparedAssetUpload | null;
bannerUpload: PreparedAssetUpload | null;
}) {
if (assetPrep.avatarUpload) {
await this.deps.entityAssetService.rollbackAssetUpload(assetPrep.avatarUpload);
}
if (assetPrep.bannerUpload) {
await this.deps.entityAssetService.rollbackAssetUpload(assetPrep.bannerUpload);
updates[hash] = prepared.newHash;
preparedAssets.push(prepared);
}
return preparedAssets;
}
}
@@ -157,14 +157,7 @@ export class OAuth2ApplicationsRequestService {
body: BotProfileUpdateRequest,
): Promise<BotProfileResponse> {
try {
const result = await this.applicationService.updateBotProfile(userId, createApplicationID(applicationId), {
username: body.username,
discriminator: body.discriminator,
avatar: body.avatar,
banner: body.banner,
bio: body.bio,
botFlags: body.bot_flags,
});
const result = await this.applicationService.updateBotProfile(userId, createApplicationID(applicationId), body);
return mapBotProfileToResponse(result.user);
} catch (err) {
if (err instanceof ApplicationNotOwnedError) {