mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 17:26:17 +00:00
fix: Move error reporting from Vercel Serverless to Express
This commit is contained in:
@@ -1,203 +0,0 @@
|
|||||||
import type { VercelRequest, VercelResponse } from '@vercel/node'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vercel Serverless Function for Error Reporting
|
|
||||||
* Receives error reports and forwards to Discord webhook
|
|
||||||
* Rate limited: 10 req/min/IP
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ErrorReportPayload {
|
|
||||||
error: string
|
|
||||||
stack?: string
|
|
||||||
context: {
|
|
||||||
version: string
|
|
||||||
platform: string
|
|
||||||
arch: string
|
|
||||||
nodeVersion: string
|
|
||||||
timestamp: string
|
|
||||||
botMode?: string
|
|
||||||
}
|
|
||||||
additionalContext?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DiscordEmbed {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
color: number
|
|
||||||
fields: Array<{ name: string; value: string; inline: boolean }>
|
|
||||||
timestamp: string
|
|
||||||
footer: { text: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limiting configuration (in-memory, resets on cold start)
|
|
||||||
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
|
|
||||||
const RATE_LIMIT_WINDOW_MS = 60 * 1000 // 1 minute
|
|
||||||
const RATE_LIMIT_MAX_REQUESTS = 10 // 10 requests per minute per IP
|
|
||||||
|
|
||||||
// Discord color constants
|
|
||||||
const DISCORD_COLOR_RED = 0xdc143c
|
|
||||||
const DISCORD_AVATAR_URL = 'https://raw.githubusercontent.com/LightZirconite/Microsoft-Rewards-Bot/refs/heads/main/assets/logo.png'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if IP is rate limited
|
|
||||||
*/
|
|
||||||
function isRateLimited(ip: string, secret?: string): boolean {
|
|
||||||
// Bypass rate limit if valid secret provided
|
|
||||||
const validSecret = process.env.RATE_LIMIT_SECRET
|
|
||||||
if (secret && validSecret && secret === validSecret) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
const entry = rateLimitMap.get(ip)
|
|
||||||
|
|
||||||
if (!entry || now > entry.resetTime) {
|
|
||||||
// Reset or create new entry
|
|
||||||
rateLimitMap.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS })
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.count >= RATE_LIMIT_MAX_REQUESTS) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.count++
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build Discord embed from error report
|
|
||||||
*/
|
|
||||||
function buildDiscordEmbed(payload: ErrorReportPayload): DiscordEmbed {
|
|
||||||
const { error, stack, context } = payload
|
|
||||||
|
|
||||||
const osPlatform = (() => {
|
|
||||||
switch (context.platform) {
|
|
||||||
case 'win32': return '🪟 Windows'
|
|
||||||
case 'darwin': return '🍎 macOS'
|
|
||||||
case 'linux': return '🐧 Linux'
|
|
||||||
default: return context.platform
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
|
|
||||||
const embed: DiscordEmbed = {
|
|
||||||
title: '🐛 Community Error Report',
|
|
||||||
description: `\`\`\`js\n${error.slice(0, 700)}\n\`\`\``,
|
|
||||||
color: DISCORD_COLOR_RED,
|
|
||||||
fields: [
|
|
||||||
{ name: '📦 Version', value: context.version === 'unknown' ? '⚠️ Unknown' : `v${context.version}`, inline: true },
|
|
||||||
{ name: '🤖 Bot Mode', value: context.botMode || 'UNKNOWN', inline: true },
|
|
||||||
{ name: '💻 OS Platform', value: `${osPlatform} ${context.arch}`, inline: true },
|
|
||||||
{ name: '⚙️ Node.js', value: context.nodeVersion, inline: true },
|
|
||||||
{ name: '🕐 Timestamp', value: new Date(context.timestamp).toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' }), inline: false }
|
|
||||||
],
|
|
||||||
timestamp: context.timestamp,
|
|
||||||
footer: { text: 'Community Error Reporting • Vercel Serverless • Non-sensitive data only' }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stack) {
|
|
||||||
const truncated = stack.slice(0, 900)
|
|
||||||
const wasTruncated = stack.length > 900
|
|
||||||
embed.fields.push({
|
|
||||||
name: '📋 Stack Trace' + (wasTruncated ? ' (truncated)' : ''),
|
|
||||||
value: `\`\`\`js\n${truncated}${wasTruncated ? '\n... (truncated for display)' : ''}\n\`\`\``,
|
|
||||||
inline: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return embed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main handler for error reporting endpoint
|
|
||||||
*/
|
|
||||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
||||||
// Enable CORS for all origins (error reporting should be accessible)
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
||||||
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
|
||||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Rate-Limit-Secret')
|
|
||||||
|
|
||||||
// Handle preflight requests
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
return res.status(200).end()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only accept POST requests
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
return res.status(405).json({ error: 'Method not allowed', message: 'Only POST requests are accepted' })
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get client IP for rate limiting
|
|
||||||
const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
|
|
||||||
(req.headers['x-real-ip'] as string) ||
|
|
||||||
'unknown'
|
|
||||||
|
|
||||||
// Get rate limit secret from headers
|
|
||||||
const rateLimitSecret = req.headers['x-rate-limit-secret'] as string | undefined
|
|
||||||
|
|
||||||
// Check rate limit
|
|
||||||
if (isRateLimited(ip, rateLimitSecret)) {
|
|
||||||
return res.status(429).json({
|
|
||||||
error: 'Rate limit exceeded',
|
|
||||||
message: 'Maximum 10 requests per minute per IP. Please try again later.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate Discord webhook URL is configured
|
|
||||||
const webhookUrl = process.env.DISCORD_ERROR_WEBHOOK_URL
|
|
||||||
if (!webhookUrl) {
|
|
||||||
console.error('[ErrorReporting] DISCORD_ERROR_WEBHOOK_URL not configured')
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Configuration error',
|
|
||||||
message: 'Discord webhook not configured. Please contact the administrator.'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate request body
|
|
||||||
const payload = req.body as ErrorReportPayload
|
|
||||||
if (!payload || !payload.error || !payload.context) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Invalid payload',
|
|
||||||
message: 'Missing required fields: error, context'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Discord embed
|
|
||||||
const embed = buildDiscordEmbed(payload)
|
|
||||||
|
|
||||||
// Send to Discord webhook
|
|
||||||
const discordPayload = {
|
|
||||||
username: 'Microsoft-Rewards-Bot Error Reporter',
|
|
||||||
avatar_url: DISCORD_AVATAR_URL,
|
|
||||||
embeds: [embed]
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(webhookUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(discordPayload)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('[ErrorReporting] Discord webhook failed:', response.status, response.statusText)
|
|
||||||
return res.status(502).json({
|
|
||||||
error: 'Discord webhook failed',
|
|
||||||
message: `HTTP ${response.status}: ${response.statusText}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Error report sent successfully'
|
|
||||||
})
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[ErrorReporting] Unexpected error:', error)
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Internal server error',
|
|
||||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -93,6 +93,56 @@ export class DashboardServer {
|
|||||||
res.json({ status: 'ok', uptime: process.uptime() })
|
res.json({ status: 'ok', uptime: process.uptime() })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Error reporting endpoint (community error collection)
|
||||||
|
this.app.post('/api/report-error', this.apiLimiter, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const webhookUrl = process.env.DISCORD_ERROR_WEBHOOK_URL
|
||||||
|
if (!webhookUrl) {
|
||||||
|
dashLog('Error reporting: DISCORD_ERROR_WEBHOOK_URL not configured', 'warn')
|
||||||
|
return res.status(503).json({ error: 'Error reporting service unavailable' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = req.body
|
||||||
|
if (!payload?.error) {
|
||||||
|
return res.status(400).json({ error: 'Invalid payload' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (use native axios, not AxiosClient)
|
||||||
|
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 })
|
||||||
|
|
||||||
|
dashLog('Error report sent to Discord', 'log')
|
||||||
|
return res.json({ success: true, message: 'Error report received' })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
dashLog(`Error reporting failed: ${error instanceof Error ? error.message : String(error)}`, 'error')
|
||||||
|
return res.status(500).json({ error: 'Failed to send error report' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Serve dashboard UI
|
// Serve dashboard UI
|
||||||
this.app.get('/', this.dashboardLimiter, (_req, res) => {
|
this.app.get('/', this.dashboardLimiter, (_req, res) => {
|
||||||
const indexPath = path.join(__dirname, '../../public/index.html')
|
const indexPath = path.join(__dirname, '../../public/index.html')
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"functions": {
|
|
||||||
"api/**/*.ts": {
|
|
||||||
"runtime": "@vercel/node@3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user