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';
5
6import BotClient from './services/Client';
7import { ALLOWED_ORIGINS, PORT, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX } from './config';
8import rateLimit from 'express-rate-limit';
9import authenticateApiKey from './middlewares/verifyApiKey';
10import status from './routes/status';
11import authRoutes from './routes/auth';
12import todosRoutes from './routes/todos';
13import apiKeysRoutes from './routes/apiKeys';
14import remindersRoutes from './routes/reminders';
15import { resetOldStrikes } from './utils/userStrikes';
16import logger from './utils/logger';
17
18config();
19
20process.on('uncaughtException', (err) => {
21 logger.error('Uncaught Exception:', err);
22 process.exit(1);
23});
24process.on('unhandledRejection', (reason, promise) => {
25 logger.error('🔥 Unhandled Rejection at:', promise);
26 logger.error('📄 Reason:', reason);
27});
28
29const app = e();
30const startTime = Date.now();
31
32app.use(helmet());
33app.use(
34 cors({
35 origin: ALLOWED_ORIGINS
36 ? ALLOWED_ORIGINS.split(',')
37 : ['http://localhost:3000', 'http://localhost:8080'],
38 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
39 allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization', 'Cache-Control', 'Pragma'],
40 credentials: true,
41 maxAge: 86400,
42 }),
43);
44app.set('trust proxy', 1);
45app.use(
46 rateLimit({
47 windowMs: RATE_LIMIT_WINDOW_MS,
48 max: RATE_LIMIT_MAX,
49 message: { error: 'Too many requests, please try again later.' },
50 standardHeaders: true,
51 legacyHeaders: false,
52 }),
53);
54
55app.use(e.json({ limit: '10mb' }));
56app.use(e.urlencoded({ extended: true, limit: '10mb' }));
57
58app.use((req, res, next) => {
59 res.setHeader('X-Content-Type-Options', 'nosniff');
60 res.setHeader('X-Frame-Options', 'DENY');
61 if (req.path.startsWith('/api/')) {
62 res.setHeader('Content-Security-Policy', "default-src 'none'");
63 res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
64 res.setHeader('Pragma', 'no-cache');
65 res.setHeader('Expires', '0');
66 res.setHeader('Surrogate-Control', 'no-store');
67 } else {
68 res.setHeader(
69 'Content-Security-Policy',
70 "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:",
71 );
72 }
73 next();
74});
75
76const bot = new BotClient();
77bot.init();
78
79app.use('/api/auth', authRoutes);
80app.use('/api/todos', todosRoutes);
81app.use('/api/user/api-keys', apiKeysRoutes);
82app.use('/api/reminders', remindersRoutes);
83
84app.use('/api/status', authenticateApiKey, status(bot));
85
86app.get('/health', (req, res) => {
87 res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
88});
89
90app.use(e.static('web/dist'));
91
92app.get('*', (req, res) => {
93 if (req.path.startsWith('/api/')) {
94 return res.status(404).json({ error: 'Not found' });
95 }
96 res.sendFile('index.html', { root: 'web/dist' });
97});
98
99setInterval(
100 () => {
101 resetOldStrikes().catch(logger.error);
102 },
103 60 * 60 * 1000,
104);
105
106const server = app.listen(PORT, async () => {
107 logger.debug('Aethel is live on', `http://localhost:${PORT}`);
108
109 const { sendDeploymentNotification } = await import('./utils/sendDeploymentNotification');
110 await sendDeploymentNotification(startTime);
111});
112
113process.on('SIGTERM', () => {
114 logger.info('SIGTERM received. Shutting down gracefully...');
115 server.close(() => {
116 logger.info('Server closed');
117 process.exit(0);
118 });
119});
120
121process.on('SIGINT', () => {
122 logger.info('SIGINT received. Shutting down gracefully...');
123 server.close(() => {
124 logger.info('Server closed');
125 process.exit(0);
126 });
127});