Enhance bot functionality with AutoMod, Logging, Welcome, and Verification features
- Implemented AutoMod capabilities including spam filtering, anti-raid, and anti-nuke actions. - Added Logging module to track moderation actions and events across the bot. - Introduced Welcome module for customizable welcome messages and autoroles. - Developed Verification system with captcha and role assignment features. - Updated Prisma schema to include new models for AutoMod, Logging, Welcome, and Verification. - Enhanced command localization for new features in both German and English. - Improved health server to handle captcha verification requests. - Added new environment variable for public base URL to support captcha links.
This commit is contained in:
@@ -1,19 +1,120 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import { env } from './env.js';
|
||||
import { redis } from './redis.js';
|
||||
import {
|
||||
deleteCaptchaChallenge,
|
||||
getCaptchaChallenge,
|
||||
hashCaptchaAnswer
|
||||
} from './modules/verification/service.js';
|
||||
import { verificationQueue } from './queues.js';
|
||||
|
||||
function readBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk) => chunks.push(chunk));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function parseFormBody(body: string): Record<string, string> {
|
||||
return Object.fromEntries(new URLSearchParams(body));
|
||||
}
|
||||
|
||||
function renderCaptchaPage(token: string, question: string, error?: string): string {
|
||||
const errorBlock = error ? `<p style="color:#ef4444">${error}</p>` : '';
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nexumi Verification</title>
|
||||
<style>
|
||||
body { font-family: Inter, sans-serif; background:#111827; color:#f9fafb; display:flex; justify-content:center; align-items:center; min-height:100vh; margin:0; }
|
||||
form { background:#1f2937; padding:2rem; border-radius:12px; width:min(420px, 90vw); }
|
||||
input, button { width:100%; padding:0.75rem; margin-top:0.75rem; border-radius:8px; border:1px solid #374151; background:#111827; color:#f9fafb; }
|
||||
button { background:#6366f1; border:none; cursor:pointer; font-weight:600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form method="POST" action="/verify/captcha">
|
||||
<h1>Nexumi Verification</h1>
|
||||
<p>Solve: <strong>${question} = ?</strong></p>
|
||||
${errorBlock}
|
||||
<input type="hidden" name="token" value="${token}" />
|
||||
<input type="number" name="answer" required autofocus />
|
||||
<button type="submit">Verify</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function handleCaptchaGet(url: URL, res: ServerResponse): Promise<void> {
|
||||
const token = url.searchParams.get('token');
|
||||
if (!token) {
|
||||
res.statusCode = 400;
|
||||
res.end('Missing token');
|
||||
return;
|
||||
}
|
||||
const challenge = await getCaptchaChallenge(redis, token);
|
||||
if (!challenge) {
|
||||
res.statusCode = 404;
|
||||
res.end('Captcha expired');
|
||||
return;
|
||||
}
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(renderCaptchaPage(token, challenge.question));
|
||||
}
|
||||
|
||||
async function handleCaptchaPost(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
const body = await readBody(req);
|
||||
const form = parseFormBody(body);
|
||||
const token = form.token;
|
||||
const answer = form.answer;
|
||||
if (!token || !answer) {
|
||||
res.statusCode = 400;
|
||||
res.end('Missing fields');
|
||||
return;
|
||||
}
|
||||
const challenge = await getCaptchaChallenge(redis, token);
|
||||
if (!challenge) {
|
||||
res.statusCode = 404;
|
||||
res.end('Captcha expired');
|
||||
return;
|
||||
}
|
||||
if (hashCaptchaAnswer(answer) !== challenge.answerHash) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end(renderCaptchaPage(token, challenge.question, 'Wrong answer. Try again.'));
|
||||
return;
|
||||
}
|
||||
await deleteCaptchaChallenge(redis, token);
|
||||
await verificationQueue.add(
|
||||
'verificationComplete',
|
||||
{ guildId: challenge.guildId, userId: challenge.userId },
|
||||
{ removeOnComplete: 1000, removeOnFail: 500 }
|
||||
);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.end('<html><body style="font-family:sans-serif;background:#111827;color:#f9fafb;display:flex;justify-content:center;align-items:center;min-height:100vh"><div><h1>Verified</h1><p>You can return to Discord.</p></div></body></html>');
|
||||
}
|
||||
|
||||
export function startHealthServer(): void {
|
||||
const server = createServer((req, res) => {
|
||||
const server = createServer(async (req, res) => {
|
||||
if (!req.url) {
|
||||
res.statusCode = 400;
|
||||
res.end('Bad Request');
|
||||
return;
|
||||
}
|
||||
if (req.url === '/health') {
|
||||
const url = new URL(req.url, `http://127.0.0.1:${env.HEALTH_PORT}`);
|
||||
|
||||
if (url.pathname === '/health') {
|
||||
res.statusCode = 200;
|
||||
res.end('ok');
|
||||
return;
|
||||
}
|
||||
if (req.url === '/metrics') {
|
||||
|
||||
if (url.pathname === '/metrics') {
|
||||
const auth = req.headers.authorization;
|
||||
if (!env.METRICS_TOKEN || auth !== `Bearer ${env.METRICS_TOKEN}`) {
|
||||
res.statusCode = 401;
|
||||
@@ -24,6 +125,17 @@ export function startHealthServer(): void {
|
||||
res.end('# Prometheus metrics scaffold\nnexumi_up 1\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/verify/captcha' && req.method === 'GET') {
|
||||
await handleCaptchaGet(url, res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/verify/captcha' && req.method === 'POST') {
|
||||
await handleCaptchaPost(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user