/* * 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 . */ import '~/instrument'; import {serve} from '@hono/node-server'; import * as Sentry from '@sentry/node'; import {Hono} from 'hono'; import {logger} from 'hono/logger'; import {Redis} from 'ioredis'; import {registerAdminControllers} from '~/admin/controllers'; import {AuthController} from '~/auth/AuthController'; import {Config} from '~/Config'; import {ChannelController} from '~/channel/ChannelController'; import type {StreamPreviewService} from '~/channel/services/StreamPreviewService'; import {DebugController} from '~/debug/DebugController'; import {DownloadController} from '~/download/DownloadController'; import {AppErrorHandler, AppNotFoundHandler} from '~/Errors'; import {InvalidApiOriginError} from '~/errors/InvalidApiOriginError'; import {FavoriteMemeController} from '~/favorite_meme/FavoriteMemeController'; import {GatewayController} from '~/gateway/GatewayController'; import {GuildController} from '~/guild/GuildController'; import {initializeMetricsService} from '~/infrastructure/MetricsService'; import {StorageService} from '~/infrastructure/StorageService'; import {InstanceController} from '~/instance/InstanceController'; import {InviteController} from '~/invite/InviteController'; import {Logger} from '~/Logger'; import {AuditLogMiddleware} from '~/middleware/AuditLogMiddleware'; import {IpBanMiddleware, ipBanCache} from '~/middleware/IpBanMiddleware'; import {MetricsMiddleware} from '~/middleware/MetricsMiddleware'; import {PendingManualVerificationMiddleware} from '~/middleware/PendingManualVerificationMiddleware'; import {RequestCacheMiddleware} from '~/middleware/RequestCacheMiddleware'; import {RequireXForwardedForMiddleware} from '~/middleware/RequireXForwardedForMiddleware'; import {ensureVoiceResourcesInitialized} from '~/middleware/ServiceMiddleware'; import {UserMiddleware} from '~/middleware/UserMiddleware'; import {initializeOAuth} from '~/oauth/init'; import {OAuth2ApplicationsController} from '~/oauth/OAuth2ApplicationsController'; import {OAuth2Controller} from '~/oauth/OAuth2Controller'; import {registerPackControllers} from '~/pack/controllers'; import {ReadStateController} from '~/read_state/ReadStateController'; import {ReportController} from '~/report/ReportController'; import {RpcController} from '~/rpc/RpcController'; import {SearchController} from '~/search/controllers/SearchController'; import {StripeController} from '~/stripe/StripeController'; import {VisionarySlotInitializer} from '~/stripe/VisionarySlotInitializer'; import {TenorController} from '~/tenor/TenorController'; import {TestHarnessController} from '~/test/TestHarnessController'; import {ThemeController} from '~/theme/ThemeController'; import {UserController} from '~/user/UserController'; import {VoiceDataInitializer} from '~/voice/VoiceDataInitializer'; import {WebhookController} from '~/webhook/WebhookController'; import type {AdminService} from './admin/AdminService'; import type {AdminArchiveService} from './admin/services/AdminArchiveService'; import type {AuthService} from './auth/AuthService'; import type {AuthMfaService} from './auth/services/AuthMfaService'; import type {DesktopHandoffService} from './auth/services/DesktopHandoffService'; import type {UserID} from './BrandedTypes'; import type {IChannelRepository} from './channel/IChannelRepository'; import type {ChannelService} from './channel/services/ChannelService'; import type {ScheduledMessageService} from './channel/services/ScheduledMessageService'; import type {FavoriteMemeService} from './favorite_meme/FavoriteMemeService'; import type {FeatureFlagService} from './feature_flag/FeatureFlagService'; import type {GuildService} from './guild/services/GuildService'; import type {EmbedService} from './infrastructure/EmbedService'; import type {EntityAssetService} from './infrastructure/EntityAssetService'; import type {ICacheService} from './infrastructure/ICacheService'; import type {IEmailService} from './infrastructure/IEmailService'; import type {IGatewayService} from './infrastructure/IGatewayService'; import type {IMediaService} from './infrastructure/IMediaService'; import type {IRateLimitService} from './infrastructure/IRateLimitService'; import type {IStorageService} from './infrastructure/IStorageService'; import type {ITenorService} from './infrastructure/ITenorService'; import type {LiveKitWebhookService} from './infrastructure/LiveKitWebhookService'; import {RedisAccountDeletionQueueService} from './infrastructure/RedisAccountDeletionQueueService'; import type {RedisActivityTracker} from './infrastructure/RedisActivityTracker'; import type {SnowflakeService} from './infrastructure/SnowflakeService'; import type {UserCacheService} from './infrastructure/UserCacheService'; import type {InviteService} from './invite/InviteService'; import type {AuthSession, User} from './Models'; import type {RequestCache} from './middleware/RequestCacheMiddleware'; import {ServiceMiddleware} from './middleware/ServiceMiddleware'; import type {ApplicationService} from './oauth/ApplicationService'; import type {BotAuthService} from './oauth/BotAuthService'; import type {OAuth2Service} from './oauth/OAuth2Service'; import type {IApplicationRepository} from './oauth/repositories/IApplicationRepository'; import type {IOAuth2TokenRepository} from './oauth/repositories/IOAuth2TokenRepository'; import type {PackRepository} from './pack/PackRepository'; import type {PackService} from './pack/PackService'; import type {ReadStateService} from './read_state/ReadStateService'; import type {ReportService} from './report/ReportService'; import type {RpcService} from './rpc/RpcService'; import type {StripeService} from './stripe/StripeService'; import type {IUserRepository} from './user/IUserRepository'; import type {EmailChangeService} from './user/services/EmailChangeService'; import {UserRepository} from './user/UserRepository'; import type {UserService} from './user/UserService'; import type {SendGridWebhookService} from './webhook/SendGridWebhookService'; import type {WebhookService} from './webhook/WebhookService'; import type {IWorkerService} from './worker/IWorkerService'; export interface HonoEnv { Variables: { user: User; adminService: AdminService; adminArchiveService: AdminArchiveService; adminUserId: UserID; adminUserAcls: Set; authTokenType?: 'session' | 'bearer' | 'bot'; authViaCookie?: boolean; authToken?: string; authUserId?: string; oauthBearerToken?: string; oauthBearerScopes?: Set; oauthBearerUserId?: UserID; auditLogReason: string | null; authMfaService: AuthMfaService; authService: AuthService; authSession: AuthSession; desktopHandoffService: DesktopHandoffService; cacheService: ICacheService; channelService: ChannelService; channelRepository: IChannelRepository; streamPreviewService: StreamPreviewService; emailService: IEmailService; emailChangeService: EmailChangeService; embedService: EmbedService; entityAssetService: EntityAssetService; favoriteMemeService: FavoriteMemeService; gatewayService: IGatewayService; guildService: GuildService; packService: PackService; packRepository: PackRepository; inviteService: InviteService; liveKitWebhookService?: LiveKitWebhookService; mediaService: IMediaService; rateLimitService: IRateLimitService; readStateService: ReadStateService; redisActivityTracker: RedisActivityTracker; reportService: ReportService; requestCache: RequestCache; rpcService: RpcService; snowflakeService: SnowflakeService; storageService: IStorageService; tenorService: ITenorService; userCacheService: UserCacheService; userRepository: IUserRepository; userService: UserService; sendGridWebhookService: SendGridWebhookService; webhookService: WebhookService; workerService: IWorkerService; scheduledMessageService: ScheduledMessageService; stripeService: StripeService; applicationService: ApplicationService; oauth2Service: OAuth2Service; applicationRepository: IApplicationRepository; oauth2TokenRepository: IOAuth2TokenRepository; botAuthService: BotAuthService; sudoModeValid: boolean; sudoModeToken: string | null; featureFlagService: FeatureFlagService; }; } export type HonoApp = typeof app; const routes = new Hono({strict: true}); routes.use( logger((message: string, ...rest: Array) => { Logger.info(rest.length > 0 ? `${message} ${rest.join(' ')}` : message); }), ); if (Config.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(MetricsMiddleware); routes.use(AuditLogMiddleware); routes.use(RequireXForwardedForMiddleware()); routes.use(RequestCacheMiddleware); routes.use(ServiceMiddleware); routes.use(UserMiddleware); routes.use(PendingManualVerificationMiddleware); routes.use('*', async (ctx, next) => { const user = ctx.get('user'); const clientIp = ctx.req.header('X-Forwarded-For')?.split(',')[0]?.trim(); Sentry.setUser({ id: user?.id.toString(), username: user?.username, email: user?.email ?? undefined, ip_address: clientIp, }); return next(); }); routes.onError(AppErrorHandler); routes.notFound(AppNotFoundHandler); routes.get('/_health', async (ctx) => ctx.text('OK')); GatewayController(routes); DebugController(routes); registerAdminControllers(routes); AuthController(routes); ChannelController(routes); InstanceController(routes); DownloadController(routes); FavoriteMemeController(routes); InviteController(routes); registerPackControllers(routes); ReadStateController(routes); ReportController(routes); RpcController(routes); GuildController(routes); SearchController(routes); TenorController(routes); ThemeController(routes); if (Config.dev.testModeEnabled) { TestHarnessController(routes); } UserController(routes); WebhookController(routes); OAuth2Controller(routes); OAuth2ApplicationsController(routes); if (!Config.instance.selfHosted) { StripeController(routes); } const app = new Hono({strict: true}); app.route('/v1', routes); app.route('/', routes); app.onError(AppErrorHandler); app.notFound(AppNotFoundHandler); await ipBanCache.initialize(); initializeMetricsService(Config.metrics.host ?? null); await initializeOAuth(); try { const redis = new Redis(Config.redis.url); const userRepository = new UserRepository(); const redisDeletionQueue = new RedisAccountDeletionQueueService(redis, userRepository); if (await redisDeletionQueue.needsRebuild()) { Logger.warn('Redis deletion queue needs rebuild, rebuilding...'); await redisDeletionQueue.rebuildState(); } else { Logger.info('Redis deletion queue state is healthy'); } await redis.quit(); } catch (error) { Logger.error({error}, 'Failed to verify Redis deletion queue state'); throw error; } if (Config.nodeEnv === 'development') { const storageService = new StorageService(); await storageService.createBucket(Config.s3.buckets.cdn, true); await storageService.createBucket(Config.s3.buckets.uploads); await storageService.createBucket(Config.s3.buckets.reports); await storageService.createBucket(Config.s3.buckets.harvests); await storageService.createBucket(Config.s3.buckets.downloads, true); await storageService.purgeBucket(Config.s3.buckets.uploads); } Logger.info( { search_enabled: Config.search.enabled, meilisearch_url: Config.search.url, meilisearch_api_key_set: !!Config.search.apiKey, }, 'Search configuration loaded', ); if (Config.search.enabled) { const {initializeMeilisearch} = await import('~/Meilisearch'); await initializeMeilisearch(); } if (Config.voice.enabled && Config.voice.autoCreateDummyData) { const voiceDataInitializer = new VoiceDataInitializer(); await voiceDataInitializer.initialize(); await ensureVoiceResourcesInitialized(); } if (Config.dev.testModeEnabled && Config.stripe.enabled) { const visionarySlotInitializer = new VisionarySlotInitializer(); await visionarySlotInitializer.initialize(); } serve({ fetch: app.fetch, hostname: '0.0.0.0', port: Config.port, }); Logger.info({port: Config.port}, `Fluxer API listening on http://0.0.0.0:${Config.port}`);