import type { Express } from "express"; import { createServer, type Server } from "http"; import { WebSocketServer, WebSocket } from "ws"; import { storage } from "./storage"; import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@shared/schema"; import { CanvasExporter } from "./export"; export async function registerRoutes(app: Express): Promise { const httpServer = createServer(app); const wss = new WebSocketServer({ server: httpServer, path: '/ws' }); let connectedUsers = new Set(); // Initialize canvas exporter const exporter = new CanvasExporter(storage); exporter.startAutoExport(); // API Routes app.get("/api/pixels", async (req, res) => { try { const pixels = await storage.getAllPixels(); res.json(pixels); } catch (error) { res.status(500).json({ message: "Failed to fetch pixels" }); } }); app.get("/api/config", async (req, res) => { try { const config = await storage.getCanvasConfig(); res.json(config); } catch (error) { res.status(500).json({ message: "Failed to fetch config" }); } }); // Config is now read-only from file // Remove the POST endpoint for config updates app.post("/api/pixels", async (req, res) => { try { const pixelData = insertPixelSchema.parse(req.body); const config = await storage.getCanvasConfig(); // Validate coordinates if (pixelData.x < 0 || pixelData.x >= config.canvasWidth || pixelData.y < 0 || pixelData.y >= config.canvasHeight) { return res.status(400).json({ message: "Pixel coordinates out of bounds" }); } // Check cooldown unless events are enabled if (!config.enableAutomaticEvents) { const cooldown = await storage.getUserCooldown(pixelData.userId); if (cooldown && cooldown.cooldownEnds > new Date()) { return res.status(429).json({ message: "Cooldown active" }); } // Set new cooldown const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000)); await storage.setUserCooldown({ userId: pixelData.userId, cooldownEnds: cooldownEnd, }); } const pixel = await storage.placePixel(pixelData); // Broadcast pixel placement to all connected clients broadcast({ type: "pixel_placed", data: { x: pixel.x, y: pixel.y, color: pixel.color, userId: pixel.userId, username: pixel.username, timestamp: pixel.createdAt.toISOString(), }, }); res.json(pixel); } catch (error) { res.status(400).json({ message: "Invalid pixel data" }); } }); app.get("/api/recent", async (req, res) => { try { const limit = parseInt(req.query.limit as string) || 10; const recent = await storage.getRecentPlacements(limit); res.json(recent); } catch (error) { res.status(500).json({ message: "Failed to fetch recent placements" }); } }); app.get("/api/cooldown/:userId", async (req, res) => { try { const { userId } = req.params; const cooldown = await storage.getUserCooldown(userId); const config = await storage.getCanvasConfig(); if (!cooldown || config.enableAutomaticEvents) { return res.json({ remainingSeconds: 0 }); } const remaining = Math.max(0, Math.ceil((cooldown.cooldownEnds.getTime() - Date.now()) / 1000)); res.json({ remainingSeconds: remaining }); } catch (error) { res.status(500).json({ message: "Failed to fetch cooldown" }); } }); // WebSocket handling function broadcast(message: WSMessage) { const messageStr = JSON.stringify(message); connectedUsers.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.send(messageStr); } }); } function broadcastUserCount() { broadcast({ type: "user_count", data: { count: connectedUsers.size }, }); } wss.on('connection', (ws, req) => { console.log(`New WebSocket connection from ${req.socket.remoteAddress}`); connectedUsers.add(ws); broadcastUserCount(); // Send current user count immediately ws.send(JSON.stringify({ type: "user_count", data: { count: connectedUsers.size }, })); ws.on('close', (code, reason) => { console.log(`WebSocket disconnected: ${code} ${reason}`); connectedUsers.delete(ws); broadcastUserCount(); }); ws.on('error', (error) => { console.error('WebSocket error:', error); connectedUsers.delete(ws); broadcastUserCount(); }); ws.on('pong', () => { // Keep connection alive }); }); // Keep connections alive with ping/pong const pingInterval = setInterval(() => { connectedUsers.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); } }); }, 30000); return httpServer; }