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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user