require('dotenv').config() import compression from 'compression'; import express from 'express'; import { execSync } from 'child_process'; import fs from 'fs'; import https from 'https'; import { configureAuth0 } from './auth0'; import { config } from './config'; import Database from './database/index'; import KnexDatabaseAdapter from './database/knex'; import GetShareRequestHandler from './endpoints/get-share'; import HealthRequestHandler from './endpoints/health'; import DeleteChatRequestHandler from './endpoints/delete-chat'; import ElevenLabsTTSProxyRequestHandler from './endpoints/service-proxies/elevenlabs/text-to-speech'; import ElevenLabsVoicesProxyRequestHandler from './endpoints/service-proxies/elevenlabs/voices'; import OpenAIProxyRequestHandler from './endpoints/service-proxies/openai'; import SessionRequestHandler from './endpoints/session'; import ShareRequestHandler from './endpoints/share'; import ObjectStore from './object-store/index'; import S3ObjectStore from './object-store/s3'; import SQLiteObjectStore from './object-store/sqlite'; import { configurePassport } from './passport'; import SyncRequestHandler, { getNumUpdatesProcessedIn5Minutes } from './endpoints/sync'; import LegacySyncRequestHandler from './endpoints/sync-legacy'; import { getActiveUsersInLast5Minutes } from './endpoints/base'; import { formatTime } from './utils'; process.on('unhandledRejection', (reason, p) => { console.error('Unhandled Rejection at: Promise', p, 'reason:', reason); }); if (process.env.CI) { setTimeout(() => process.exit(), 10000); } const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001; export default class ChatServer { authProvider = 'local'; app: express.Application; objectStore: ObjectStore = process.env.S3_BUCKET ? new S3ObjectStore() : new SQLiteObjectStore(); database: Database = new KnexDatabaseAdapter(); constructor() { this.app = express(); } async initialize() { //const { default: helmet } = await import('helmet'); //this.app.use(helmet()); this.app.use(express.urlencoded({ extended: false })); if (config.auth0?.clientID && config.auth0?.issuer && config.publicSiteURL) { console.log('Configuring Auth0.'); this.authProvider = 'auth0'; configureAuth0(this); } else { console.log('Configuring Passport.'); this.authProvider = 'local'; configurePassport(this); } this.app.use(express.json({ limit: '1mb' })); this.app.use(compression({ filter: (req, res) => !req.path.includes("proxies"), })); const { default: rateLimit } = await import('express-rate-limit'); // esm this.app.get('/chatapi/health', (req, res) => new HealthRequestHandler(this, req, res)); this.app.get('/chatapi/session', rateLimit({ windowMs: 60 * 1000, max: 100 }), (req, res) => new SessionRequestHandler(this, req, res)); this.app.post('/chatapi/y-sync', rateLimit({ windowMs: 60 * 1000, max: 100 }), express.raw({ type: 'application/octet-stream', limit: '10mb' }), (req, res) => new SyncRequestHandler(this, req, res)); this.app.get('/chatapi/legacy-sync', rateLimit({ windowMs: 60 * 1000, max: 100 }), (req, res) => new LegacySyncRequestHandler(this, req, res)); this.app.use(rateLimit({ windowMs: config.rateLimit.windowMs, max: config.rateLimit.max, })); this.app.post('/chatapi/delete', (req, res) => new DeleteChatRequestHandler(this, req, res)); this.app.get('/chatapi/share/:id', (req, res) => new GetShareRequestHandler(this, req, res)); this.app.post('/chatapi/share', (req, res) => new ShareRequestHandler(this, req, res)); if (config.services?.openai?.apiKey) { this.app.post('/chatapi/proxies/openai/v1/chat/completions', (req, res) => new OpenAIProxyRequestHandler(this, req, res)); } if (config.services?.elevenlabs?.apiKey) { this.app.post('/chatapi/proxies/elevenlabs/v1/text-to-speech/:voiceID', (req, res) => new ElevenLabsTTSProxyRequestHandler(this, req, res)); this.app.get('/chatapi/proxies/elevenlabs/v1/voices', (req, res) => new ElevenLabsVoicesProxyRequestHandler(this, req, res)); } if (fs.existsSync('public')) { const match = /`; const indexFilename = "public/index.html"; let indexSource = fs.readFileSync(indexFilename, 'utf8'); indexSource = indexSource.replace(match, replace); if (fs.existsSync('./data/head.html')) { const head = fs.readFileSync('./data/head.html').toString(); indexSource = indexSource.replace('', ` ${head} `); } this.app.get('/', (req, res) => { res.send(indexSource); }); this.app.use(express.static('public')); // serve index.html for all other routes this.app.get('*', (req, res) => { res.send(indexSource); }); } await this.objectStore.initialize(); await this.database.initialize(); try { const callback = (https = false) => { console.log(`Open ${config.publicSiteURL || `http${https ? 's' : ''}://localhost:3000`} in your browser.`); }; if (config.tls?.key && config.tls?.cert) { console.log('Configuring TLS.'); const server = https.createServer({ key: fs.readFileSync(config.tls.key), cert: fs.readFileSync(config.tls.cert), }, this.app); server.listen(port, () => callback(true)); } else if (config.tls?.selfSigned) { console.log('Configuring self-signed TLS.'); if (!fs.existsSync('./data/key.pem') || !fs.existsSync('./data/cert.pem')) { execSync('sh generate-self-signed-certificate.sh'); } const server = https.createServer({ key: fs.readFileSync('./data/key.pem'), cert: fs.readFileSync('./data/cert.pem'), }, this.app); server.listen(port, callback); } else { this.app.listen(port, callback); } } catch (e) { console.log(e); } const displayStatistics = () => { const activeUsers = getActiveUsersInLast5Minutes(); const activeUsersToDisplay = activeUsers.slice(0, 10); const extraActiveUsers = activeUsers.slice(10); const numRecentUpdates = getNumUpdatesProcessedIn5Minutes(); console.log(`[${formatTime()}] ${activeUsers.length} active users and ${numRecentUpdates} updates processed in last 5m`); if (extraActiveUsers.length) { console.log(` - Active users: ${activeUsersToDisplay.join(', ')} and ${extraActiveUsers.length} more`); } else if (activeUsers.length) { console.log(` - Active users: ${activeUsersToDisplay.join(', ')}`); } } setInterval(displayStatistics, 1000 * 60 * 5); setTimeout(displayStatistics, 1000 * 30); } } new ChatServer().initialize();