import express from "express"; import multer from "multer"; import path from "path"; import fs from "fs"; import cors from "cors"; import { fileURLToPath } from "url"; import { Vonage } from "@vonage/server-sdk"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(cors()); app.use(express.json({ limit: "10mb" })); // Serve uploaded audio const uploadDir = path.join(__dirname, "uploads"); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir); app.use("/uploads", express.static(uploadDir)); // Multer for audio upload const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadDir), filename: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase() || ".webm"; cb(null, `voice-${Date.now()}${ext}`); } }); const upload = multer({ storage }); // Vonage init const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: fs.readFileSync(path.join(__dirname, "private.key")) }); const VONAGE_FROM_NUMBER = process.env.VONAGE_FROM_NUMBER; const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL; // In-memory call status store const callStatusStore = {}; // Upload endpoint app.post("/api/upload-audio", upload.single("audio"), (req, res) => { if (!req.file) return res.status(400).json({ error: "No file" }); const audioUrl = `${PUBLIC_BASE_URL}/uploads/${req.file.filename}`; return res.json({ audioUrl }); }); // Voice blast endpoint app.post("/api/voice-blast", async (req, res) => { const { numbers, audioUrl, amd = true, loop = 1 } = req.body; if (!Array.isArray(numbers) || numbers.length === 0) { return res.status(400).json({ error: "numbers required" }); } if (!audioUrl) return res.status(400).json({ error: "audioUrl required" }); const results = []; for (const to of numbers) { try { const ncco = [ { action: "stream", streamUrl: [audioUrl], loop } ]; const callReq = { to: [{ type: "phone", number: to }], from: { type: "phone", number: VONAGE_FROM_NUMBER }, ncco, machineDetection: amd ? "continue" : undefined, eventUrl: [`${PUBLIC_BASE_URL}/api/call-events`] }; const resp = await vonage.voice.createOutboundCall(callReq); results.push({ to, callUuid: resp.uuid, status: "queued" }); } catch (e) { results.push({ to, error: e.message }); } } return res.json({ results }); }); // Call events webhook app.post("/api/call-events", express.json(), (req, res) => { const { uuid, to, status } = req.body; if (uuid && to && status) { callStatusStore[uuid] = { to: to.number, status, timestamp: new Date().toISOString() }; } res.status(200).end(); }); // Call status endpoint app.get("/api/call-status", (req, res) => { const list = Object.entries(callStatusStore).map(([uuid, info]) => ({ callUuid: uuid, ...info })); res.json(list); }); const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`Voice server running on ${PORT}`); });

Voice broadcast

Idle