Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
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});