| import express from "express"; |
| import { createServer } from "http"; |
| import { WebSocketServer } from "ws"; |
|
|
| const app = express(); |
| const server = createServer(app); |
| const wss = new WebSocketServer({ server, path: "/audio-stream" }); |
|
|
| app.use(express.static("public", { index: false })); |
| app.use(express.json()); |
|
|
| const PORT = process.env.PORT || 7860; |
| const TELNYX_API_KEY = process.env.TELNYX_API_KEY; |
| const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID; |
| const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER; |
| const AI_SERVER_URL = process.env.AI_SERVER_URL; |
| const TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER; |
| const TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID; |
|
|
| const ACTIVE_VOICE = "Enceladus_3"; |
| const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}]`, ...args); |
|
|
| let pendingCalls = new Map(); |
| let activeLeadIds = new Map(); |
| const incomingCallDebounce = new Map(); |
|
|
| const MY_CELL_NUMBER = "+2347066923490"; |
|
|
| |
| const MU_LAW_TABLE = new Int16Array(256); |
| for (let i = 0; i < 256; i++) { |
| const sign = (i & 0x80) ? -1 : 1; |
| const exponent = (~i >> 4) & 0x07; |
| const mantissa = ~i & 0x0f; |
| const sample = ((mantissa << 3) + 132) << exponent; |
| MU_LAW_TABLE[i] = sign * (sample - 132); |
| } |
| function decodeMuLaw(buffer) { |
| const out = new Int16Array(buffer.length); |
| for (let i = 0; i < buffer.length; i++) out[i] = MU_LAW_TABLE[buffer[i]]; |
| return out; |
| } |
| function calculateRMS(pcm) { |
| let sum = 0; |
| for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i]; |
| return Math.sqrt(sum / pcm.length); |
| } |
|
|
| const sendTelnyxCmd = async (callId, action, body = {}) => { |
| try { |
| const url = callId ? `https://api.telnyx.com/v2/calls/${callId}/actions/${action}` : `https://api.telnyx.com/v2/calls`; |
| const response = await fetch(url, { |
| method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` }, |
| body: JSON.stringify(body) |
| }); |
| if (!response.ok) log("TELNYX CMD ERROR", `[${action}] ${await response.text()}`); |
| return response; |
| } catch (e) { log("TELNYX CMD NETWORK ERROR", e.message); } |
| }; |
|
|
| app.post("/api/call-action", async (req, res) => { |
| const { call_control_id, action, lead_id } = req.body; |
| if (!pendingCalls.has(call_control_id)) return res.status(404).json({ error: "Call not pending" }); |
| if (lead_id) activeLeadIds.set(call_control_id, lead_id); |
| pendingCalls.set(call_control_id, action); |
| |
| if (action === "reject") { |
| await sendTelnyxCmd(call_control_id, "hangup"); |
| pendingCalls.delete(call_control_id); |
| } else { |
| await sendTelnyxCmd(call_control_id, "answer"); |
| } |
| res.json({ ok: true }); |
| }); |
|
|
| app.post("/api/hangup", async (req, res) => { |
| await sendTelnyxCmd(req.body.call_control_id, "hangup"); |
| res.json({ ok: true }); |
| }); |
|
|
| app.post("/api/send-sms", async (req, res) => { |
| log("SMS", `Attempting outbound SMS to ${req.body.to}...`); |
| try { |
| const sendRequest = async (fromNumber) => { |
| const payload = { from: fromNumber, to: req.body.to, text: req.body.text }; |
| if (TELNYX_MESSAGING_PROFILE_ID) payload.messaging_profile_id = TELNYX_MESSAGING_PROFILE_ID; |
| return await fetch("https://api.telnyx.com/v2/messages", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` }, body: JSON.stringify(payload) }); |
| }; |
|
|
| let response = await sendRequest(TELNYX_ALPHA_SENDER); |
| let data = await response.json(); |
|
|
| if (!response.ok && data.errors && data.errors.some(e => e.code === '40305' || e.code === '40012')) { |
| log("SMS WARNING", `Alpha Sender rejected. Falling back to standard US number ${TELNYX_FROM_NUMBER}...`); |
| response = await sendRequest(TELNYX_FROM_NUMBER); |
| data = await response.json(); |
| } |
|
|
| if (!response.ok) log("SMS ERROR", JSON.stringify(data)); |
| else log("SMS SUCCESS", `Message accepted by Telnyx for delivery to ${req.body.to}`); |
| res.json(data); |
| } catch(e) { res.status(500).json({ error: e.message }); } |
| }); |
|
|
| app.post("/dial", async (req, res) => { |
| const { to, lead_id, action = "manual" } = req.body; |
| |
| try { |
| let callPayload = { connection_id: TELNYX_CONNECTION_ID, from: TELNYX_FROM_NUMBER }; |
|
|
| if (action === "manual") { |
| log("DIAL", `[Manual] Dialing Agent First (${MY_CELL_NUMBER})...`); |
| |
| const stateObj = { type: "agent_first", customer_number: to, lead_id: lead_id }; |
| callPayload.to = MY_CELL_NUMBER; |
| callPayload.client_state = Buffer.from(JSON.stringify(stateObj)).toString("base64"); |
| } else { |
| log("DIAL", `[AI] Dialing Customer directly (${to})...`); |
| |
| callPayload.to = to; |
| } |
|
|
| const response = await fetch("https://api.telnyx.com/v2/calls", { |
| method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` }, |
| body: JSON.stringify(callPayload) |
| }); |
| const data = await response.json(); |
| if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail }); |
|
|
| const ccid = data.data.call_control_id; |
| pendingCalls.set(ccid, action); |
| if (lead_id) activeLeadIds.set(ccid, lead_id); |
|
|
| await fetch(`${AI_SERVER_URL}/api/log-outbound`, { |
| method: "POST", headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ call_control_id: ccid, action, lead_id }) |
| }).catch(() => {}); |
|
|
| res.json({ ok: true, call_control_id: ccid }); |
| } catch (err) { res.status(500).json({ ok: false, error: err.message }); } |
| }); |
|
|
| app.post("/webhook/call", async (req, res) => { |
| res.sendStatus(200); |
| const type = req.body?.data?.event_type; |
| const payload = req.body?.data?.payload; |
| const callId = payload?.call_control_id; |
| if (!type) return; |
|
|
| switch (type) { |
| case "call.initiated": |
| if (payload?.direction === "incoming") { |
| const fromNumber = payload.from; |
| const now = Date.now(); |
| if (incomingCallDebounce.has(fromNumber) && now - incomingCallDebounce.get(fromNumber) < 10000) return; |
| incomingCallDebounce.set(fromNumber, now); |
|
|
| log("INCOMING", `From: ${fromNumber}`); |
| pendingCalls.set(callId, "waiting"); |
| await fetch(`${AI_SERVER_URL}/api/incoming-call`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ call_control_id: callId, from: fromNumber }) }).catch(()=>{}); |
| } |
| break; |
|
|
| case "call.answered": |
| |
| if (payload?.client_state) { |
| try { |
| const stateStr = Buffer.from(payload.client_state, "base64").toString("utf8"); |
| const stateObj = JSON.parse(stateStr); |
|
|
| |
| if (stateObj.type === "agent_first") { |
| log("BRIDGING", `Agent picked up! Dialing Customer (${stateObj.customer_number})...`); |
| |
| |
| await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false }); |
| |
| |
| await sendTelnyxCmd(callId, "playback_start", { audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav", target_legs: "self" }); |
|
|
| |
| const nextState = { type: "customer_answered", leg_a_id: callId }; |
| await sendTelnyxCmd(null, "dial", { |
| to: stateObj.customer_number, |
| from: TELNYX_FROM_NUMBER, |
| connection_id: TELNYX_CONNECTION_ID, |
| client_state: Buffer.from(JSON.stringify(nextState)).toString("base64") |
| }); |
| return; |
| } |
|
|
| |
| if (stateObj.type === "customer_answered") { |
| log("BRIDGING", `Customer picked up! Bridging them to the Agent...`); |
| |
| |
| await sendTelnyxCmd(stateObj.leg_a_id, "playback_stop"); |
| |
| |
| await sendTelnyxCmd(callId, "bridge", { call_control_id: stateObj.leg_a_id }); |
| return; |
| } |
| } catch (e) { log("STATE ERROR", "Failed to parse client state JSON."); } |
| } |
|
|
| |
| const actionType = pendingCalls.get(callId) || "ai"; |
| if (actionType === "ai") { |
| log("AI CONNECTING", `Starting stream for ${callId}`); |
| await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false }); |
| await sendTelnyxCmd(callId, "playback_start", { audio_url: `http://${req.headers.host}/ambience.mp3`, target_legs: "caller", loop: "infinity" }); |
| await sendTelnyxCmd(callId, "streaming_start", { stream_url: `wss://${req.headers.host}/audio-stream`, stream_track: "inbound_track" }); |
| await sendTelnyxCmd(callId, "transcription_start", { transcription_engine: "Telnyx", transcription_tracks: "inbound" }); |
| setTimeout(async () => { log("HANGUP", `Enforcing 90s limit on ${callId}`); await sendTelnyxCmd(callId, "hangup"); }, 90000); |
| } |
| break; |
|
|
| case "call.transcription": |
| if (!payload?.transcription_data?.is_final) break; |
| try { |
| const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: payload.transcription_data.transcript.trim(), call_id: callId, lead_id: activeLeadIds.get(callId) || null }) }); |
| if (aiRes.ok) { |
| const aiData = await aiRes.json(); |
| if (aiData.speak_url) await sendTelnyxCmd(callId, "playback_start", { audio_url: aiData.speak_url, target_legs: "self" }); |
| } |
| } catch (e) { log("AI_SERVER_ERROR", e.message); } |
| break; |
| |
| case "call.recording.saved": |
| if (payload.recording_urls?.mp3) { |
| await fetch(`${AI_SERVER_URL}/api/save-recording`, { |
| method: "POST", headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ call_control_id: callId, recording_url: payload.recording_urls.mp3 }) |
| }).then(async (r) => { if (!r.ok) log("RECORDING ERROR", `AI Server status: ${r.status}`); }).catch((err) => log("RECORDING NET ERROR", err.message)); |
| } |
| break; |
|
|
| case "call.hangup": |
| log("HANGUP", `Call ended. Reason: ${payload.hangup_cause}`); |
| pendingCalls.delete(callId); |
| activeLeadIds.delete(callId); |
| await fetch(`${AI_SERVER_URL}/api/call-ended`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ call_control_id: callId }) }).catch(() => {}); |
| break; |
| } |
| }); |
|
|
| wss.on("connection", (ws) => { |
| let isSpeaking = false; |
| let silenceFrames = 0; |
| let callControlId = null; |
| let hasPlayedFiller = false; |
| let hasGreeted = false; |
| let totalNoiseFrames = 0; |
|
|
| ws.on("message", async (raw) => { |
| try { |
| const msg = JSON.parse(raw); |
| if (msg.event === "start") { |
| callControlId = msg.start?.call_control_id; |
| setTimeout(async () => { |
| if (hasGreeted || !callControlId) return; |
| hasGreeted = true; |
| const greetingPhrase = "Hi, this is Nathan."; |
| await sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(greetingPhrase)}`, target_legs: "self" }); |
| fetch(`${AI_SERVER_URL}/api/register-greeting`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ call_id: callControlId, phrase: greetingPhrase, lead_id: activeLeadIds.get(callControlId) || null }) }).catch(() => {}); |
| }, 1500); |
| } |
|
|
| if (msg.event === "media") { |
| const pcm16 = decodeMuLaw(Buffer.from(msg.media.payload, "base64")); |
| const rms = calculateRMS(pcm16); |
|
|
| if (rms > 500) { |
| hasGreeted = true; |
| silenceFrames = 0; |
| totalNoiseFrames++; |
| if (!isSpeaking) { |
| isSpeaking = true; |
| hasPlayedFiller = false; |
| await sendTelnyxCmd(callControlId, "playback_stop"); |
| } |
| } else if (isSpeaking) { |
| silenceFrames++; |
| if (silenceFrames > 35 && !hasPlayedFiller) { |
| hasPlayedFiller = true; |
| isSpeaking = false; |
| if (totalNoiseFrames > 40) { |
| const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."]; |
| sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(fillers[Math.floor(Math.random() * fillers.length)])}`, target_legs: "self" }).catch(() => {}); |
| } |
| totalNoiseFrames = 0; |
| } |
| } |
| } |
| } catch {} |
| }); |
| }); |
|
|
| server.listen(PORT, "0.0.0.0", () => { |
| log("STARTUP", `Telecom routing server running on port ${PORT}`); |
| }); |