// SPDX-License-Identifier: AGPL-3.0-or-later import {randomUUID} from 'node:crypto'; import {initCassandra, shutdownCassandra} from '@pkgs/cassandra/src/Client'; import {ensureGeoipDatabaseOnStartup} from '@pkgs/geoip/src/GeoipStartup'; import {JetStreamConnectionManager} from '@pkgs/nats/src/JetStreamConnectionManager'; import {getDefaultPostgresClient, initPostgres, shutdownPostgres} from '@pkgs/postgres/src/Client'; import type {APIConfig} from '../config/APIConfig'; import {hasDatabaseQueryExecutor, setDatabaseQueryExecutor} from '../database/CassandraQueryExecution'; import {ensurePostgresKvSchema, PostgresKvQueryExecutor} from '../database/PostgresKvQueryExecutor'; import {GuildDataRepository} from '../guild/repositories/GuildDataRepository'; import type {ILogger} from '../ILogger'; import {JobLedgerRepository} from '../jobs/JobLedgerRepository'; import {startAbuseReplicationSubscriber, stopAbuseReplicationSubscriber} from '../middleware/AbusiveIpAutoBanner'; import {ipBanCache} from '../middleware/IpBanMiddleware'; import {initializeServiceSingletons, shutdownReportService} from '../middleware/ServiceMiddleware'; import { ensureVoiceResourcesInitialized, getKVClient, getSnowflakeService, setInjectedWorkerService, } from '../middleware/ServiceRegistry'; import { getCacheService, getInstanceConfigRepository, getKVAccountDeletionQueue, getReportRepository, getUserRepository, } from '../middleware/ServiceSingletons'; import {torExitListCache} from '../middleware/TorExitListCache'; import {initializeSearch, shutdownSearch} from '../SearchFactory'; import {warmupAdminSearchIndexes} from '../search/SearchWarmup'; import {VisionarySlotInitializer} from '../stripe/VisionarySlotInitializer'; import {VoiceDataInitializer} from '../voice/VoiceDataInitializer'; import {JetStreamWorkerQueue} from '../worker/JetStreamWorkerQueue'; import {WorkerService} from '../worker/WorkerService'; let jsConnectionManager: JetStreamConnectionManager | null = null; function unsupportedDatabaseBackend(backend: never): never { throw new Error(`Unsupported database backend during shutdown: ${String(backend)}`); } export function createInitializer(config: APIConfig, logger: ILogger): () => Promise { return async (): Promise => { try { logger.info('Initializing API service...'); const geoipStartupResult = await ensureGeoipDatabaseOnStartup({ geoip: config.geoip, s3Config: { endpoint: config.s3.endpoint, region: config.s3.region, accessKeyId: config.s3.accessKeyId, secretAccessKey: config.s3.secretAccessKey, }, }); if (geoipStartupResult.mode === 's3') { logger.info( { maxmind_db_path: geoipStartupResult.maxmindDbPath, city: geoipStartupResult.city, asn: geoipStartupResult.asn, s3_bucket: geoipStartupResult.bucket, s3_key: geoipStartupResult.key, }, 'GeoIP databases downloaded from S3', ); } if (config.database.backend === 'postgres' && !hasDatabaseQueryExecutor()) { await initPostgres(config.postgres); const postgres = getDefaultPostgresClient(); await ensurePostgresKvSchema(postgres); setDatabaseQueryExecutor(new PostgresKvQueryExecutor(postgres)); logger.info('Postgres KV client initialized'); } else if (config.database.backend === 'postgres') { logger.info('Using injected database query executor for tests'); } if (config.database.backend === 'cassandra' && !hasDatabaseQueryExecutor()) { await initCassandra({ hosts: config.cassandra.hosts.split(',').filter(Boolean), port: config.cassandra.port, keyspace: config.cassandra.keyspace, localDc: config.cassandra.localDc, username: config.cassandra.username || undefined, password: config.cassandra.password || undefined, }); logger.info('Cassandra client initialized'); } else if (config.database.backend === 'cassandra') { logger.info('Using injected database query executor for tests'); } const kvClient = getKVClient(); ipBanCache.setRefreshSubscriber(kvClient); await ipBanCache.initialize(); logger.info('IP ban cache initialized'); await startAbuseReplicationSubscriber(kvClient); logger.info('Abusive-IP auto-banner replication started'); torExitListCache.setKvClient(kvClient); await torExitListCache.initialize(); logger.info('Tor exit list cache initialized'); const {urlBlocklistCache} = await import('../middleware/UrlBlocklistCache'); urlBlocklistCache.setRefreshSubscriber(kvClient); const {getStorageService} = await import('../middleware/ServiceSingletons'); urlBlocklistCache.setStorageService(getStorageService()); await urlBlocklistCache.initialize(); logger.info('URL blocklist cache initialized'); const {fileShaCache} = await import('../middleware/FileShaCache'); fileShaCache.setRefreshSubscriber(kvClient); await fileShaCache.initialize(); logger.info('File SHA blocklist cache initialized'); const {phraseBlocklistCache} = await import('../middleware/PhraseBlocklistCache'); phraseBlocklistCache.setRefreshSubscriber(kvClient); await phraseBlocklistCache.initialize(); logger.info('Phrase blocklist cache initialized'); const {bannedAvatarHashCache} = await import('../middleware/BannedAvatarHashCache'); bannedAvatarHashCache.setRefreshSubscriber(kvClient); await bannedAvatarHashCache.initialize(); logger.info('Banned avatar hash cache initialized'); const {profileSubstringBlocklistCache} = await import('../middleware/ProfileSubstringBlocklistCache'); profileSubstringBlocklistCache.setRefreshSubscriber(kvClient); await profileSubstringBlocklistCache.initialize(); logger.info('Profile substring blocklist cache initialized'); await initializeServiceSingletons(); logger.info('Service singletons initialized'); if (!config.dev.testModeEnabled) { jsConnectionManager = new JetStreamConnectionManager({ url: config.nats.jetStreamUrl, token: config.nats.authToken || undefined, name: 'api-worker', }); await jsConnectionManager.connect(); const workerQueue = new JetStreamWorkerQueue(jsConnectionManager); await workerQueue.ensureStream(); setInjectedWorkerService(new WorkerService(workerQueue, getSnowflakeService(), new JobLedgerRepository())); logger.info('JetStream worker service initialized'); } try { const kvDeletionQueue = getKVAccountDeletionQueue(); if (await kvDeletionQueue.needsRebuild()) { logger.info('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('Initializing search indexes...'); let searchInitialized = false; try { await initializeSearch(getCacheService()); searchInitialized = true; logger.info('Search initialized'); } catch (error) { logger.error({error}, 'Search initialisation failed'); throw error; } if (searchInitialized) { const warmupLockKey = 'fluxer:search:warmup:admin'; const warmupLockToken = randomUUID(); const warmupLockTtlSeconds = 60 * 60; const acquiredWarmupLock = await kvClient.acquireLock(warmupLockKey, warmupLockToken, warmupLockTtlSeconds); if (!acquiredWarmupLock) { logger.info('Another API instance is warming search indexes, skipping warmup'); } else { try { await warmupAdminSearchIndexes({ userRepository: getUserRepository(), guildRepository: new GuildDataRepository(), reportRepository: getReportRepository(), 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 = getInstanceConfigRepository(); 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'); } catch (error) { logger.error({error}, 'API service initialization failed'); await createShutdown(config, logger)(); throw error; } }; } export function createShutdown(config: APIConfig, logger: ILogger): () => Promise { return async (): Promise => { logger.info('Shutting down API service...'); if (jsConnectionManager) { try { await jsConnectionManager.drain(); jsConnectionManager = null; } catch (error) { logger.error({error}, 'Error draining JetStream worker connection'); } } setInjectedWorkerService(undefined); 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'); } try { await stopAbuseReplicationSubscriber(); logger.info('Abusive-IP auto-banner replication stopped'); } catch (error) { logger.error({error}, 'Error stopping abusive-IP auto-banner replication'); } try { torExitListCache.shutdown(); logger.info('Tor exit list cache shut down'); } catch (error) { logger.error({error}, 'Error shutting down Tor exit list cache'); } try { getInstanceConfigRepository().shutdown(); logger.info('Instance config repository shut down'); } catch (error) { logger.error({error}, 'Error shutting down instance config repository'); } try { shutdownReportService(); logger.info('Report service shut down'); } catch (error) { logger.error({error}, 'Error shutting down report service'); } setDatabaseQueryExecutor(null); switch (config.database.backend) { case 'postgres': try { await shutdownPostgres(); logger.info('Postgres client shut down'); } catch (error) { logger.error({error}, 'Error shutting down Postgres client'); } break; case 'cassandra': try { await shutdownCassandra(); logger.info('Cassandra client shut down'); } catch (error) { logger.error({error}, 'Error shutting down Cassandra client'); } break; default: unsupportedDatabaseBackend(config.database.backend); } logger.info('API service shutdown complete'); }; }