Update app.js
Browse files
app.js
CHANGED
|
@@ -23,6 +23,7 @@ const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}
|
|
| 23 |
let pendingCalls = new Map();
|
| 24 |
let activeLeadIds = new Map();
|
| 25 |
const incomingCallDebounce = new Map();
|
|
|
|
| 26 |
|
| 27 |
// ── MU-LAW VAD ──
|
| 28 |
const MU_LAW_TABLE = new Int16Array(256);
|
|
@@ -111,17 +112,35 @@ app.post("/api/send-sms", async (req, res) => {
|
|
| 111 |
|
| 112 |
app.post("/dial", async (req, res) => {
|
| 113 |
const { to, lead_id, action = "manual" } = req.body;
|
| 114 |
-
log("DIAL", `Outbound to ${to} (Mode: ${action})`);
|
|
|
|
| 115 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
const response = await fetch("https://api.telnyx.com/v2/calls", {
|
| 117 |
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
|
| 118 |
-
body: JSON.stringify({ connection_id: TELNYX_CONNECTION_ID, to, from: TELNYX_FROM_NUMBER })
|
| 119 |
});
|
|
|
|
| 120 |
const data = await response.json();
|
| 121 |
if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
|
| 122 |
|
| 123 |
const ccid = data.data.call_control_id;
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
if (lead_id) activeLeadIds.set(ccid, lead_id);
|
| 126 |
|
| 127 |
await fetch(`${AI_SERVER_URL}/api/log-outbound`, {
|
|
@@ -165,14 +184,15 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 165 |
break;
|
| 166 |
|
| 167 |
case "call.answered":
|
|
|
|
| 168 |
if (payload?.client_state) {
|
| 169 |
const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
|
| 170 |
-
log("BRIDGING", `
|
| 171 |
|
| 172 |
-
// Stop the ringing sound
|
| 173 |
await sendTelnyxCmd(legACallId, "playback_stop");
|
| 174 |
|
| 175 |
-
//
|
| 176 |
await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
|
| 177 |
break;
|
| 178 |
}
|
|
@@ -181,24 +201,27 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 181 |
|
| 182 |
await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
|
| 183 |
|
| 184 |
-
if (actionType === "
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
//
|
| 188 |
await sendTelnyxCmd(callId, "playback_start", {
|
| 189 |
audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav",
|
| 190 |
target_legs: "self"
|
| 191 |
});
|
| 192 |
|
| 193 |
-
//
|
| 194 |
-
const MY_CELL_NUMBER = "+2347066923490";
|
| 195 |
await sendTelnyxCmd(null, "dial", {
|
| 196 |
-
to:
|
| 197 |
from: TELNYX_FROM_NUMBER,
|
| 198 |
connection_id: TELNYX_CONNECTION_ID,
|
| 199 |
-
|
|
|
|
| 200 |
});
|
| 201 |
-
|
|
|
|
| 202 |
log("AI CONNECTING", `Starting stream for ${callId}`);
|
| 203 |
await sendTelnyxCmd(callId, "playback_start", { audio_url: `http://${req.headers.host}/ambience.mp3`, target_legs: "caller", loop: "infinity" });
|
| 204 |
await sendTelnyxCmd(callId, "streaming_start", { stream_url: `wss://${req.headers.host}/audio-stream`, stream_track: "inbound_track" });
|
|
@@ -236,6 +259,7 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 236 |
log("HANGUP", `Call ended. Reason: ${payload.hangup_cause}`);
|
| 237 |
pendingCalls.delete(callId);
|
| 238 |
activeLeadIds.delete(callId);
|
|
|
|
| 239 |
await fetch(`${AI_SERVER_URL}/api/call-ended`, {
|
| 240 |
method: "POST", headers: { "Content-Type": "application/json" },
|
| 241 |
body: JSON.stringify({ call_control_id: callId })
|
|
|
|
| 23 |
let pendingCalls = new Map();
|
| 24 |
let activeLeadIds = new Map();
|
| 25 |
const incomingCallDebounce = new Map();
|
| 26 |
+
const agentDestinations = new Map(); // Stores where to route after Agent answers
|
| 27 |
|
| 28 |
// ── MU-LAW VAD ──
|
| 29 |
const MU_LAW_TABLE = new Int16Array(256);
|
|
|
|
| 112 |
|
| 113 |
app.post("/dial", async (req, res) => {
|
| 114 |
const { to, lead_id, action = "manual" } = req.body;
|
| 115 |
+
log("DIAL", `Outbound request to ${to} (Mode: ${action})`);
|
| 116 |
+
|
| 117 |
try {
|
| 118 |
+
let targetNumber = to;
|
| 119 |
+
const MY_CELL_NUMBER = "+2348069234734"; // Your agent number
|
| 120 |
+
|
| 121 |
+
// REVERSE DIALING: If manual, call the agent's cell phone FIRST
|
| 122 |
+
if (action === "manual") {
|
| 123 |
+
targetNumber = MY_CELL_NUMBER;
|
| 124 |
+
log("DIAL", `Reverse dialing: Calling Agent ${MY_CELL_NUMBER} first...`);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
const response = await fetch("https://api.telnyx.com/v2/calls", {
|
| 128 |
method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
|
| 129 |
+
body: JSON.stringify({ connection_id: TELNYX_CONNECTION_ID, to: targetNumber, from: TELNYX_FROM_NUMBER })
|
| 130 |
});
|
| 131 |
+
|
| 132 |
const data = await response.json();
|
| 133 |
if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
|
| 134 |
|
| 135 |
const ccid = data.data.call_control_id;
|
| 136 |
+
|
| 137 |
+
if (action === "manual") {
|
| 138 |
+
pendingCalls.set(ccid, "agent_leg");
|
| 139 |
+
agentDestinations.set(ccid, { to, lead_id }); // Save the lead's number for when you answer
|
| 140 |
+
} else {
|
| 141 |
+
pendingCalls.set(ccid, action);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
if (lead_id) activeLeadIds.set(ccid, lead_id);
|
| 145 |
|
| 146 |
await fetch(`${AI_SERVER_URL}/api/log-outbound`, {
|
|
|
|
| 184 |
break;
|
| 185 |
|
| 186 |
case "call.answered":
|
| 187 |
+
// This block fires when the LEAD finally answers the second leg
|
| 188 |
if (payload?.client_state) {
|
| 189 |
const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
|
| 190 |
+
log("BRIDGING", `Lead picked up! Stopping ringback and bridging you in instantly...`);
|
| 191 |
|
| 192 |
+
// Stop the ringing sound playing in your (the agent's) ear
|
| 193 |
await sendTelnyxCmd(legACallId, "playback_stop");
|
| 194 |
|
| 195 |
+
// Connect you to the lead immediately
|
| 196 |
await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
|
| 197 |
break;
|
| 198 |
}
|
|
|
|
| 201 |
|
| 202 |
await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
|
| 203 |
|
| 204 |
+
if (actionType === "agent_leg") {
|
| 205 |
+
// This block fires when YOU answer your cell phone
|
| 206 |
+
const dest = agentDestinations.get(callId);
|
| 207 |
+
log("BRIDGING", `You answered! Now automatically dialing the Lead at ${dest?.to}...`);
|
| 208 |
|
| 209 |
+
// Play ringing to you so you know it's dialing the lead
|
| 210 |
await sendTelnyxCmd(callId, "playback_start", {
|
| 211 |
audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav",
|
| 212 |
target_legs: "self"
|
| 213 |
});
|
| 214 |
|
| 215 |
+
// Tell Telnyx to call the lead now
|
|
|
|
| 216 |
await sendTelnyxCmd(null, "dial", {
|
| 217 |
+
to: dest.to,
|
| 218 |
from: TELNYX_FROM_NUMBER,
|
| 219 |
connection_id: TELNYX_CONNECTION_ID,
|
| 220 |
+
// We pass your callId so the system knows who to connect the lead to when they answer
|
| 221 |
+
client_state: Buffer.from(callId).toString("base64")
|
| 222 |
});
|
| 223 |
+
|
| 224 |
+
} else if (actionType === "ai") {
|
| 225 |
log("AI CONNECTING", `Starting stream for ${callId}`);
|
| 226 |
await sendTelnyxCmd(callId, "playback_start", { audio_url: `http://${req.headers.host}/ambience.mp3`, target_legs: "caller", loop: "infinity" });
|
| 227 |
await sendTelnyxCmd(callId, "streaming_start", { stream_url: `wss://${req.headers.host}/audio-stream`, stream_track: "inbound_track" });
|
|
|
|
| 259 |
log("HANGUP", `Call ended. Reason: ${payload.hangup_cause}`);
|
| 260 |
pendingCalls.delete(callId);
|
| 261 |
activeLeadIds.delete(callId);
|
| 262 |
+
agentDestinations.delete(callId); // Clean up the map
|
| 263 |
await fetch(`${AI_SERVER_URL}/api/call-ended`, {
|
| 264 |
method: "POST", headers: { "Content-Type": "application/json" },
|
| 265 |
body: JSON.stringify({ call_control_id: callId })
|