Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at main 4.6 kB view raw
1import { config } from 'dotenv'; 2import e from 'express'; 3import helmet from 'helmet'; 4import cors from 'cors'; 5import path from 'path'; 6import { fileURLToPath } from 'url'; 7import BotClient from './services/Client'; 8import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config'; 9import rateLimit from 'express-rate-limit'; 10import authenticateApiKey from './middlewares/verifyApiKey'; 11import { authenticateToken } from './middlewares/auth'; 12import status from './routes/status'; 13import authRoutes from './routes/auth'; 14import todosRoutes from './routes/todos'; 15import apiKeysRoutes from './routes/apiKeys'; 16import remindersRoutes from './routes/reminders'; 17import voteWebhookRoutes from './routes/voteWebhook'; 18import { resetOldStrikes } from './utils/userStrikes'; 19import logger from './utils/logger'; 20 21config(); 22 23process.on('uncaughtException', (err) => { 24 logger.error('Uncaught Exception:', err); 25 process.exit(1); 26}); 27process.on('unhandledRejection', (reason, promise) => { 28 logger.error('🔥 Unhandled Rejection at:', promise); 29 logger.error('📄 Reason:', reason); 30}); 31 32const app = e(); 33const startTime = Date.now(); 34const __filename = fileURLToPath(import.meta.url); 35const __dirname = path.dirname(__filename); 36const distPath = path.resolve(__dirname, '../web/dist'); 37 38app.use(helmet()); 39app.use( 40 cors({ 41 origin: ALLOWED_ORIGINS 42 ? ALLOWED_ORIGINS.split(',') 43 : ['http://localhost:3000', 'http://localhost:8080'], 44 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 45 allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization', 'Cache-Control', 'Pragma'], 46 credentials: true, 47 maxAge: 86400, 48 }), 49); 50app.set('trust proxy', 1); 51app.use( 52 rateLimit({ 53 windowMs: RATE_LIMIT_WINDOW_MS, 54 max: RATE_LIMIT_MAX, 55 message: { error: 'Too many requests, please try again later.' }, 56 standardHeaders: true, 57 legacyHeaders: false, 58 }), 59); 60 61app.use(e.json({ limit: '10mb' })); 62app.use(e.urlencoded({ extended: true, limit: '10mb' })); 63 64app.use((req, res, next) => { 65 res.setHeader('X-Content-Type-Options', 'nosniff'); 66 res.setHeader('X-Frame-Options', 'DENY'); 67 if (req.path.startsWith('/api/')) { 68 res.setHeader('Content-Security-Policy', "default-src 'none'"); 69 res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); 70 res.setHeader('Pragma', 'no-cache'); 71 res.setHeader('Expires', '0'); 72 res.setHeader('Surrogate-Control', 'no-store'); 73 } else { 74 res.setHeader( 75 'Content-Security-Policy', 76 "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:", 77 ); 78 } 79 next(); 80}); 81 82const bot = new BotClient(); 83bot.init(); 84 85app.use(async (req, res, next) => { 86 const start = process.hrtime.bigint(); 87 res.on('finish', () => { 88 const durMs = Number(process.hrtime.bigint() - start) / 1e6; 89 const safePath = req.baseUrl ? `${req.baseUrl}${req.path}` : req.path; 90 logger.debug(`API [${req.method}] ${safePath} ${res.statusCode} ${durMs.toFixed(1)}ms`); 91 }); 92 next(); 93}); 94 95app.use('/api/auth', authRoutes); 96app.use('/api/todos', todosRoutes); 97app.use('/api/user/api-keys', apiKeysRoutes); 98app.use('/api/reminders', authenticateToken, remindersRoutes); 99app.use('/api', voteWebhookRoutes); 100 101app.use('/api/status', authenticateApiKey, status(bot)); 102 103app.get('/health', (req, res) => { 104 res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); 105}); 106 107app.use(e.static(distPath, { index: false, maxAge: '1h' })); 108 109app.get(/^\/(?!api\/).*/, (req, res) => { 110 return res.sendFile(path.join(distPath, 'index.html')); 111}); 112 113app.use((req, res) => { 114 return res.status(404).json({ status: 404, message: 'Not Found' }); 115}); 116 117setInterval( 118 () => { 119 resetOldStrikes().catch(logger.error); 120 }, 121 60 * 60 * 1000, 122); 123 124const server = app.listen(PORT, async () => { 125 logger.debug('Aethel is live on', `http://localhost:${PORT}`); 126 127 const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification.js'); 128 await sendDeploymentNotification(startTime); 129}); 130 131process.on('SIGTERM', () => { 132 logger.info('SIGTERM received. Shutting down gracefully...'); 133 server.close(() => { 134 logger.info('Server closed'); 135 process.exit(0); 136 }); 137}); 138 139process.on('SIGINT', () => { 140 logger.info('SIGINT received. Shutting down gracefully...'); 141 server.close(() => { 142 logger.info('Server closed'); 143 process.exit(0); 144 }); 145});