refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {randomUUID} from 'node:crypto';
import type {APIConfig} from '@fluxer/api/src/config/APIConfig';
import {GuildDataRepository} from '@fluxer/api/src/guild/repositories/GuildDataRepository';
import type {ILogger} from '@fluxer/api/src/ILogger';
import {KVAccountDeletionQueueService} from '@fluxer/api/src/infrastructure/KVAccountDeletionQueueService';
import {initializeMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
import {InstanceConfigRepository} from '@fluxer/api/src/instance/InstanceConfigRepository';
import {ipBanCache} from '@fluxer/api/src/middleware/IpBanMiddleware';
import {initializeServiceSingletons} from '@fluxer/api/src/middleware/ServiceMiddleware';
import {ensureVoiceResourcesInitialized, getKVClient} from '@fluxer/api/src/middleware/ServiceRegistry';
import {ReportRepository} from '@fluxer/api/src/report/ReportRepository';
import {initializeSearch, shutdownSearch} from '@fluxer/api/src/SearchFactory';
import {warmupAdminSearchIndexes} from '@fluxer/api/src/search/SearchWarmup';
import {VisionarySlotInitializer} from '@fluxer/api/src/stripe/VisionarySlotInitializer';
import {UserRepository} from '@fluxer/api/src/user/repositories/UserRepository';
import {VoiceDataInitializer} from '@fluxer/api/src/voice/VoiceDataInitializer';
export function createInitializer(config: APIConfig, logger: ILogger): () => Promise<void> {
return async (): Promise<void> => {
logger.info('Initializing API service...');
const kvClient = getKVClient();
ipBanCache.setRefreshSubscriber(kvClient);
await ipBanCache.initialize();
logger.info('IP ban cache initialized');
initializeMetricsService();
logger.info('Metrics service initialized');
await initializeServiceSingletons();
logger.info('Service singletons initialized');
try {
const userRepository = new UserRepository();
const kvDeletionQueue = new KVAccountDeletionQueueService(kvClient, userRepository);
if (await kvDeletionQueue.needsRebuild()) {
logger.warn('KV deletion queue needs rebuild, rebuilding...');
await kvDeletionQueue.rebuildState();
} else {
logger.info('KV deletion queue state is healthy');
}
} catch (error) {
logger.error({error}, 'Failed to verify KV deletion queue state');
throw error;
}
logger.info({search_url: config.search.url}, 'Initializing search...');
await initializeSearch();
logger.info('Search initialized');
// All API replicas share the same Meilisearch cluster, so only one should warm it.
const warmupLockKey = 'fluxer:search:warmup:admin';
const warmupLockToken = randomUUID();
const warmupLockTtlSeconds = 60 * 60;
const acquiredWarmupLock = await kvClient.setnx(warmupLockKey, warmupLockToken, warmupLockTtlSeconds);
if (!acquiredWarmupLock) {
logger.info('Another API instance is warming search indexes, skipping warmup');
} else {
try {
await warmupAdminSearchIndexes({
userRepository: new UserRepository(),
guildRepository: new GuildDataRepository(),
reportRepository: new ReportRepository(),
logger,
});
} catch (error) {
logger.error({error}, 'Admin search warmup failed (continuing startup)');
} finally {
try {
await kvClient.releaseLock(warmupLockKey, warmupLockToken);
} catch (error) {
logger.warn({error}, 'Failed to release admin search warmup lock');
}
}
}
if (config.voice.enabled && config.voice.defaultRegion) {
const voiceDataInitializer = new VoiceDataInitializer();
await voiceDataInitializer.initialize();
await ensureVoiceResourcesInitialized();
logger.info('Voice data initialized');
}
if (config.dev.testModeEnabled && config.stripe.enabled) {
const visionarySlotInitializer = new VisionarySlotInitializer();
await visionarySlotInitializer.initialize();
logger.info('Stripe visionary slots initialized');
}
if (config.dev.testModeEnabled) {
const instanceConfigRepository = new InstanceConfigRepository();
try {
await instanceConfigRepository.setSsoConfig({
enabled: false,
authorizationUrl: null,
tokenUrl: null,
clientId: null,
});
logger.info('Reset SSO config to disabled for test mode');
} catch (error) {
logger.warn({error}, 'Failed to reset SSO config for test mode');
}
}
logger.info('API service initialization complete');
};
}
export function createShutdown(logger: ILogger): () => Promise<void> {
return async (): Promise<void> => {
logger.info('Shutting down API service...');
try {
await shutdownSearch();
logger.info('Search service shut down');
} catch (error) {
logger.error({error}, 'Error shutting down search service');
}
try {
ipBanCache.shutdown();
logger.info('IP ban cache shut down');
} catch (error) {
logger.error({error}, 'Error shutting down IP ban cache');
}
logger.info('API service shutdown complete');
};
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {registerAdminControllers} from '@fluxer/api/src/admin/controllers';
import {AuthController} from '@fluxer/api/src/auth/AuthController';
import {BlueskyOAuthController} from '@fluxer/api/src/bluesky/BlueskyOAuthController';
import {ChannelController} from '@fluxer/api/src/channel/ChannelController';
import type {APIConfig} from '@fluxer/api/src/config/APIConfig';
import {ConnectionController} from '@fluxer/api/src/connection/ConnectionController';
import {DonationController} from '@fluxer/api/src/donation/DonationController';
import {DownloadController} from '@fluxer/api/src/download/DownloadController';
import {FavoriteMemeController} from '@fluxer/api/src/favorite_meme/FavoriteMemeController';
import {GatewayController} from '@fluxer/api/src/gateway/GatewayController';
import {GuildController} from '@fluxer/api/src/guild/GuildController';
import {InstanceController} from '@fluxer/api/src/instance/InstanceController';
import {InviteController} from '@fluxer/api/src/invite/InviteController';
import {KlipyController} from '@fluxer/api/src/klipy/KlipyController';
import {OAuth2ApplicationsController} from '@fluxer/api/src/oauth/OAuth2ApplicationsController';
import {OAuth2Controller} from '@fluxer/api/src/oauth/OAuth2Controller';
import {registerPackControllers} from '@fluxer/api/src/pack/controllers';
import {ReadStateController} from '@fluxer/api/src/read_state/ReadStateController';
import {ReportController} from '@fluxer/api/src/report/ReportController';
import {RpcController} from '@fluxer/api/src/rpc/RpcController';
import {SearchController} from '@fluxer/api/src/search/controllers/SearchController';
import {StripeController} from '@fluxer/api/src/stripe/StripeController';
import {TenorController} from '@fluxer/api/src/tenor/TenorController';
import {TestHarnessController} from '@fluxer/api/src/test/TestHarnessController';
import {ThemeController} from '@fluxer/api/src/theme/ThemeController';
import type {HonoApp} from '@fluxer/api/src/types/HonoEnv';
import {UserController} from '@fluxer/api/src/user/controllers/UserController';
import {WebhookController} from '@fluxer/api/src/webhook/WebhookController';
export function registerControllers(routes: HonoApp, config: APIConfig): void {
GatewayController(routes);
registerAdminControllers(routes);
AuthController(routes);
ChannelController(routes);
ConnectionController(routes);
BlueskyOAuthController(routes);
InstanceController(routes);
DownloadController(routes);
FavoriteMemeController(routes);
InviteController(routes);
registerPackControllers(routes);
ReadStateController(routes);
ReportController(routes);
RpcController(routes);
GuildController(routes);
SearchController(routes);
KlipyController(routes);
TenorController(routes);
ThemeController(routes);
if (config.dev.testModeEnabled || config.nodeEnv === 'development') {
TestHarnessController(routes);
}
UserController(routes);
WebhookController(routes);
OAuth2Controller(routes);
OAuth2ApplicationsController(routes);
if (!config.instance.selfHosted) {
DonationController(routes);
StripeController(routes);
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ILogger} from '@fluxer/api/src/ILogger';
import {AuditLogMiddleware} from '@fluxer/api/src/middleware/AuditLogMiddleware';
import {ConcurrencyLimitMiddleware} from '@fluxer/api/src/middleware/ConcurrencyLimitMiddleware';
import {GuildAvailabilityMiddleware} from '@fluxer/api/src/middleware/GuildAvailabilityMiddleware';
import {IpBanMiddleware} from '@fluxer/api/src/middleware/IpBanMiddleware';
import {LocaleMiddleware} from '@fluxer/api/src/middleware/LocaleMiddleware';
import {MetricsMiddleware} from '@fluxer/api/src/middleware/MetricsMiddleware';
import {RequestCacheMiddleware} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import {RequireXForwardedForMiddleware} from '@fluxer/api/src/middleware/RequireXForwardedForMiddleware';
import {ServiceMiddleware} from '@fluxer/api/src/middleware/ServiceMiddleware';
import {UserMiddleware} from '@fluxer/api/src/middleware/UserMiddleware';
import type {HonoApp, HonoEnv} from '@fluxer/api/src/types/HonoEnv';
import {InvalidApiOriginError} from '@fluxer/errors/src/domains/core/InvalidApiOriginError';
import {applyMiddlewareStack} from '@fluxer/hono/src/middleware/MiddlewareStack';
import {createServiceTelemetry} from '@fluxer/hono/src/middleware/TelemetryAdapters';
import {formatTraceparent, getActiveSpan} from '@fluxer/telemetry/src/Tracing';
import type {Context as HonoContext} from 'hono';
export interface MiddlewarePipelineOptions {
logger: ILogger;
nodeEnv: string;
setSentryUser?: (user: {id?: string; username?: string; email?: string; ip_address?: string}) => void;
isTelemetryActive?: () => boolean;
}
const TRACEPARENT_HEADER = 'traceparent';
function attachTraceparentHeader(ctx: HonoContext<HonoEnv>): void {
const span = getActiveSpan();
if (!span) {
return;
}
const traceparent = formatTraceparent(span);
if (traceparent) {
ctx.header(TRACEPARENT_HEADER, traceparent);
}
}
export function configureMiddleware(routes: HonoApp, options: MiddlewarePipelineOptions): void {
const {logger, nodeEnv, setSentryUser, isTelemetryActive} = options;
const requestTelemetry = createServiceTelemetry({
serviceName: 'fluxer-api',
skipPaths: ['/_health', '/internal/telemetry'],
});
applyMiddlewareStack(routes, {
requestId: {},
tracing: requestTelemetry.tracing,
metrics: {
enabled: true,
collector: requestTelemetry.metricsCollector,
skipPaths: ['/_health', '/internal/telemetry'],
},
logger: {
log: (data) => {
logger.info(
{
method: data.method,
path: data.path,
status: data.status,
durationMs: data.durationMs,
},
'Request completed',
);
},
skip: ['/_health'],
},
skipErrorHandler: true,
});
if (nodeEnv === 'production') {
routes.use('*', async (ctx, next) => {
const host = ctx.req.header('host');
if (ctx.req.method !== 'GET' && (host === 'web.fluxer.app' || host === 'web.canary.fluxer.app')) {
const origin = ctx.req.header('origin');
if (!origin || origin !== `https://${host}`) {
throw new InvalidApiOriginError();
}
}
await next();
});
}
routes.use(IpBanMiddleware);
routes.use(ConcurrencyLimitMiddleware);
routes.use(MetricsMiddleware);
routes.use(AuditLogMiddleware);
routes.use(RequireXForwardedForMiddleware());
routes.use(RequestCacheMiddleware);
routes.use(ServiceMiddleware);
routes.use(UserMiddleware);
routes.use(GuildAvailabilityMiddleware);
routes.use(LocaleMiddleware);
routes.use('*', async (ctx, next) => {
attachTraceparentHeader(ctx);
await next();
});
if (setSentryUser) {
routes.use('*', async (ctx, next) => {
const user = ctx.get('user');
const clientIp = ctx.req.header('X-Forwarded-For')?.split(',')[0]?.trim();
setSentryUser({
id: user?.id.toString(),
username: user?.username,
email: user?.email ?? undefined,
ip_address: clientIp,
});
return next();
});
}
routes.get('/_health', async (ctx) => ctx.text('OK'));
if (isTelemetryActive) {
routes.get('/internal/telemetry', async (ctx) => {
return ctx.json({
telemetry_enabled: isTelemetryActive(),
service: 'fluxer_api',
timestamp: new Date().toISOString(),
});
});
}
}