diff --git a/api/index.ts b/api/index.ts new file mode 100644 index 0000000..8873a46 --- /dev/null +++ b/api/index.ts @@ -0,0 +1,104 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node' +import express from 'express' + +// In-memory rate limiting for error reporting +const rateLimitMap = new Map() +const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute +const RATE_LIMIT_MAX_REQUESTS = 10 + +function isRateLimited(ip: string): boolean { + const now = Date.now() + const record = rateLimitMap.get(ip) + + if (!record || now > record.resetTime) { + rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS }) + return false + } + + if (record.count >= RATE_LIMIT_MAX_REQUESTS) { + return true + } + + record.count++ + return false +} + +// Vercel serverless handler +export default async function handler(req: VercelRequest, res: VercelResponse) { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS') + res.setHeader('Access-Control-Allow-Headers', 'Content-Type') + + // Handle preflight + if (req.method === 'OPTIONS') { + return res.status(200).end() + } + + // Only POST allowed + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }) + } + + try { + // Rate limiting + const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() || 'unknown' + if (isRateLimited(ip)) { + return res.status(429).json({ error: 'Rate limit exceeded' }) + } + + // Check Discord webhook URL + const webhookUrl = process.env.DISCORD_ERROR_WEBHOOK_URL + if (!webhookUrl) { + console.error('[ErrorReporting] DISCORD_ERROR_WEBHOOK_URL not configured') + return res.status(503).json({ error: 'Error reporting service unavailable' }) + } + + // Validate payload + const payload = req.body + if (!payload?.error) { + return res.status(400).json({ error: 'Invalid payload: missing error field' }) + } + + // Build Discord embed + const embed = { + title: '🔴 Bot Error Report', + description: `\`\`\`\n${String(payload.error).slice(0, 1900)}\n\`\`\``, + color: 0xdc143c, + fields: [ + { name: 'Version', value: String(payload.context?.version || 'unknown'), inline: true }, + { name: 'Platform', value: String(payload.context?.platform || 'unknown'), inline: true }, + { name: 'Node', value: String(payload.context?.nodeVersion || 'unknown'), inline: true } + ], + timestamp: new Date().toISOString(), + footer: { text: 'Community Error Reporting' } + } + + if (payload.stack) { + const stackLines = String(payload.stack).split('\n').slice(0, 15).join('\n') + embed.fields.push({ + name: 'Stack Trace', + value: `\`\`\`\n${stackLines.slice(0, 1000)}\n\`\`\``, + inline: false + }) + } + + // Send to Discord + const axios = (await import('axios')).default + await axios.post(webhookUrl, { + username: 'Microsoft Rewards Bot', + avatar_url: 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/assets/logo.png', + embeds: [embed] + }, { timeout: 10000 }) + + console.log('[ErrorReporting] Report sent successfully') + return res.json({ success: true, message: 'Error report received' }) + + } catch (error) { + console.error('[ErrorReporting] Failed:', error) + return res.status(500).json({ + error: 'Failed to send error report', + message: error instanceof Error ? error.message : 'Unknown error' + }) + } +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..b1fb62a --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/api/report-error", + "destination": "/api" + } + ] +}