2026-01-01 21:05:54 +00:00

334 lines
13 KiB
TypeScript

/*
* 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 '~/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<string>;
authTokenType?: 'session' | 'bearer' | 'bot';
authViaCookie?: boolean;
authToken?: string;
authUserId?: string;
oauthBearerToken?: string;
oauthBearerScopes?: Set<string>;
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<HonoEnv>({strict: true});
routes.use(
logger((message: string, ...rest: Array<string>) => {
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<HonoEnv>({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}`);