Pepguy commited on
Commit
2ede20b
Β·
verified Β·
1 Parent(s): 0bf76da

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +30 -88
app.js CHANGED
@@ -139,41 +139,29 @@ app.get("/", (req, res) => {
139
  `);
140
  });
141
 
142
- app.get("/api/logs", (req, res) => {
143
- res.json([...callLogs].reverse());
144
- });
145
-
146
- app.get("/health", (req, res) => {
147
- res.json({ status: "ok", uptime: process.uptime() });
148
- });
149
 
150
- // ─────────────────────────────────────────────
151
- // 2. PROXY FOR TELNYX RECORDINGS
152
- // ─────────────────────────────────────────────
153
  app.get("/play-recording/:call_control_id", async (req, res) => {
154
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
155
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
156
 
157
  try {
158
  const s3Res = await fetch(logEntry.raw_telnyx_url);
159
- if (!s3Res.ok) {
160
- log("PROXY ERROR", `S3 returned ${s3Res.status}`);
161
- return res.sendStatus(s3Res.status);
162
- }
163
  const arrayBuffer = await s3Res.arrayBuffer();
164
  const buffer = Buffer.from(arrayBuffer);
 
165
  res.setHeader("Content-Type", "audio/mpeg");
166
  res.setHeader("Content-Length", buffer.length);
167
  res.send(buffer);
168
  } catch (e) {
169
- log("PROXY ERROR", e.message);
170
  res.sendStatus(500);
171
  }
172
  });
173
 
174
- // ─────────────────────────────────────────────
175
- // 3. DIAL OUT ROUTE
176
- // ─────────────────────────────────────────────
177
  app.post("/dial", async (req, res) => {
178
  const { to } = req.body;
179
  const voice = "Algieba";
@@ -214,46 +202,24 @@ app.post("/dial", async (req, res) => {
214
  }
215
  });
216
 
217
- // ─────────────────────────────────────────────
218
- // 4. MANUAL HANG UP ROUTE (FIXED)
219
- // ─────────────────────────────────────────────
220
  app.post("/call/hangup", async (req, res) => {
221
  const { call_control_id } = req.body;
222
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
223
 
224
- log("HANGUP", `Manual hangup request for ${call_control_id}`);
225
-
226
  try {
227
- const response = await fetch(
228
- `https://api.telnyx.com/v2/calls/${call_control_id}/actions/hangup`,
229
- {
230
- method: "POST",
231
- headers: {
232
- "Content-Type": "application/json",
233
- Authorization: `Bearer ${TELNYX_API_KEY}`,
234
- },
235
- // FIX: Completely stripped out client_state. Passing an empty object.
236
- body: JSON.stringify({}),
237
- }
238
- );
239
-
240
- if (!response.ok) {
241
- const errText = await response.text();
242
- log("HANGUP ERROR", `Telnyx rejected manual hangup (${response.status}): ${errText}`);
243
- return res.status(response.status).json({ ok: false, error: errText });
244
- }
245
-
246
- log("HANGUP SUCCESS", "Manual hangup command accepted.");
247
  res.json({ ok: true });
248
  } catch (err) {
249
- log("HANGUP NETWORK ERROR", err.message);
250
  res.status(500).json({ ok: false, error: err.message });
251
  }
252
  });
253
 
254
- // ─────────────────────────────────────────────
255
- // 5. TELNYX WEBHOOK (FIXED)
256
- // ─────────────────────────────────────────────
257
  app.post("/webhook/call", async (req, res) => {
258
  const type = req.body?.data?.event_type;
259
  const payload = req.body?.data?.payload;
@@ -262,7 +228,6 @@ app.post("/webhook/call", async (req, res) => {
262
 
263
  if (!type) return res.sendStatus(200);
264
 
265
- // ── FIX: Clean Telnyx Dispatcher (No base64 string breaking the API) ──
266
  const telnyxCmd = async (action, body = {}) => {
267
  try {
268
  const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, {
@@ -270,12 +235,7 @@ app.post("/webhook/call", async (req, res) => {
270
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
271
  body: JSON.stringify(body)
272
  });
273
- if (!response.ok) {
274
- const errText = await response.text();
275
- log("TELNYX CMD ERROR", `[${action}] failed (${response.status}): ${errText}`);
276
- } else {
277
- log("TELNYX CMD SUCCESS", `[${action}] accepted.`);
278
- }
279
  return response;
280
  } catch (e) {
281
  log("TELNYX CMD NETWORK ERROR", e.message);
@@ -294,9 +254,16 @@ app.post("/webhook/call", async (req, res) => {
294
 
295
  switch (type) {
296
  case "call.initiated":
297
- log("INITIATED", `from=${payload?.from} to=${payload?.to} direction=${payload?.direction}`);
298
  if (payload?.direction === "incoming") {
299
- log("INBOUND", "Answering inbound call...");
 
 
 
 
 
 
 
300
  callLogs.push({
301
  id: randomUUID(),
302
  call_control_id: callId,
@@ -317,31 +284,29 @@ app.post("/webhook/call", async (req, res) => {
317
  updateLog({ status: "in-progress" });
318
 
319
  await telnyxCmd("record_start", { format: "mp3", channels: "dual", play_beep: false });
320
-
321
  await telnyxCmd("playback_start", {
322
  audio_url: `${host}/ambience.mp3`,
323
  loop: "infinity",
324
  target_legs: "self"
325
  });
326
 
327
- log("STT", "Activating native Telnyx real-time transcription engine...");
328
  await telnyxCmd("transcription_start", {
329
  transcription_engine: "Telnyx",
330
  transcription_tracks: "inbound"
331
  });
332
 
 
333
  setTimeout(async () => {
334
  const voice = getActiveVoice();
335
- const exactPhrase = encodeURIComponent("am I speaking with the business owner?");
336
  await telnyxCmd("playback_start", {
337
- audio_url: `${AI_SERVER_URL}/play-audio/${voice}/${exactPhrase}`,
338
  target_legs: "self"
339
  });
340
  }, 1500);
341
 
342
- // ── FIX: Clean auto-hangup (no client_state) ──
343
  setTimeout(async () => {
344
- log("HANGUP", `Enforcing 60-second limit on call ${callId}`);
345
  await telnyxCmd("hangup", {});
346
  }, 60000);
347
  break;
@@ -371,50 +336,27 @@ app.post("/webhook/call", async (req, res) => {
371
  });
372
  }
373
  }
374
- } catch (e) {
375
- log("AI_SERVER_ERROR", e.message);
376
- }
377
  }
378
  break;
379
 
380
  case "call.recording.saved":
381
- const rawUrl = payload.recording_urls.mp3;
382
- log("RECORDING", `Saved. URL: ${rawUrl}`);
383
  updateLog({
384
  status: "completed",
385
- raw_telnyx_url: rawUrl,
386
  recording_url: `/play-recording/${callId}`
387
  });
388
  break;
389
 
390
  case "call.hangup":
391
  updateLog({ status: "completed" });
392
- log("CALL ENDED", `Call ${callId} has cleanly terminated.`);
393
  break;
394
  }
395
 
396
  res.sendStatus(200);
397
  });
398
 
399
- // ─── SMS WEBHOOK ─────────────────────────────
400
- app.post("/webhook/sms", (req, res) => {
401
- const type = req.body?.data?.event_type;
402
- if (type === "message.received") {
403
- log("SMS INBOUND", `from=${req.body?.data?.payload?.from?.phone_number}`);
404
- }
405
- res.sendStatus(200);
406
- });
407
-
408
- wss.on("connection", (ws, req) => {
409
- log("WS", `Legacy media connection from ${req.socket.remoteAddress}`);
410
- ws.on("message", (raw) => {});
411
- });
412
-
413
- const requiredEnv = ["TELNYX_API_KEY", "TELNYX_CONNECTION_ID", "TELNYX_FROM_NUMBER", "AI_SERVER_URL"];
414
- requiredEnv.forEach((key) => {
415
- if (!process.env[key]) log("STARTUP WARNING", `Missing env var: ${key}`);
416
- });
417
-
418
  server.listen(PORT, "0.0.0.0", () => {
419
  log("STARTUP", `Telecom Server running on port ${PORT}`);
420
  });
 
139
  `);
140
  });
141
 
142
+ app.get("/api/logs", (req, res) => res.json([...callLogs].reverse()));
143
+ app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
 
 
 
 
 
144
 
 
 
 
145
  app.get("/play-recording/:call_control_id", async (req, res) => {
146
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
147
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
148
 
149
  try {
150
  const s3Res = await fetch(logEntry.raw_telnyx_url);
151
+ if (!s3Res.ok) return res.sendStatus(s3Res.status);
152
+
 
 
153
  const arrayBuffer = await s3Res.arrayBuffer();
154
  const buffer = Buffer.from(arrayBuffer);
155
+
156
  res.setHeader("Content-Type", "audio/mpeg");
157
  res.setHeader("Content-Length", buffer.length);
158
  res.send(buffer);
159
  } catch (e) {
 
160
  res.sendStatus(500);
161
  }
162
  });
163
 
164
+ // ─── DIAL OUT ROUTE ──────────────────────────
 
 
165
  app.post("/dial", async (req, res) => {
166
  const { to } = req.body;
167
  const voice = "Algieba";
 
202
  }
203
  });
204
 
205
+ // ─── MANUAL HANG UP ROUTE ──────────────────────────
 
 
206
  app.post("/call/hangup", async (req, res) => {
207
  const { call_control_id } = req.body;
208
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
209
 
 
 
210
  try {
211
+ await fetch(`https://api.telnyx.com/v2/calls/${call_control_id}/actions/hangup`, {
212
+ method: "POST",
213
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
214
+ body: JSON.stringify({}),
215
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  res.json({ ok: true });
217
  } catch (err) {
 
218
  res.status(500).json({ ok: false, error: err.message });
219
  }
220
  });
221
 
222
+ // ─── TELNYX WEBHOOK (Turn Detection & Alerts) ────────────────
 
 
223
  app.post("/webhook/call", async (req, res) => {
224
  const type = req.body?.data?.event_type;
225
  const payload = req.body?.data?.payload;
 
228
 
229
  if (!type) return res.sendStatus(200);
230
 
 
231
  const telnyxCmd = async (action, body = {}) => {
232
  try {
233
  const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, {
 
235
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
236
  body: JSON.stringify(body)
237
  });
238
+ if (!response.ok) log("TELNYX CMD ERROR", `[${action}] failed (${response.status})`);
 
 
 
 
 
239
  return response;
240
  } catch (e) {
241
  log("TELNYX CMD NETWORK ERROR", e.message);
 
254
 
255
  switch (type) {
256
  case "call.initiated":
257
+ log("INITIATED", `from=${payload?.from} to=${payload?.to}`);
258
  if (payload?.direction === "incoming") {
259
+ log("INBOUND", "Answering inbound call & alerting AI Server...");
260
+
261
+ // ── NEW: Alert AI Server so human can pick up ──
262
+ fetch(`${AI_SERVER_URL}/api/inbound-call`, {
263
+ method: "POST", headers: { "Content-Type": "application/json" },
264
+ body: JSON.stringify({ from: payload?.from, to: payload?.to, call_id: callId })
265
+ }).catch(() => {});
266
+
267
  callLogs.push({
268
  id: randomUUID(),
269
  call_control_id: callId,
 
284
  updateLog({ status: "in-progress" });
285
 
286
  await telnyxCmd("record_start", { format: "mp3", channels: "dual", play_beep: false });
287
+
288
  await telnyxCmd("playback_start", {
289
  audio_url: `${host}/ambience.mp3`,
290
  loop: "infinity",
291
  target_legs: "self"
292
  });
293
 
 
294
  await telnyxCmd("transcription_start", {
295
  transcription_engine: "Telnyx",
296
  transcription_tracks: "inbound"
297
  });
298
 
299
+ // ── FIX: Audio Playback URL uses Query Params ──
300
  setTimeout(async () => {
301
  const voice = getActiveVoice();
302
+ const phrase = encodeURIComponent("am I speaking with the business owner?");
303
  await telnyxCmd("playback_start", {
304
+ audio_url: `${AI_SERVER_URL}/play-audio?voice=${voice}&phrase=${phrase}`,
305
  target_legs: "self"
306
  });
307
  }, 1500);
308
 
 
309
  setTimeout(async () => {
 
310
  await telnyxCmd("hangup", {});
311
  }, 60000);
312
  break;
 
336
  });
337
  }
338
  }
339
+ } catch (e) { log("AI_SERVER_ERROR", e.message); }
 
 
340
  }
341
  break;
342
 
343
  case "call.recording.saved":
 
 
344
  updateLog({
345
  status: "completed",
346
+ raw_telnyx_url: payload.recording_urls.mp3,
347
  recording_url: `/play-recording/${callId}`
348
  });
349
  break;
350
 
351
  case "call.hangup":
352
  updateLog({ status: "completed" });
353
+ log("CALL ENDED", `Call ${callId} terminated.`);
354
  break;
355
  }
356
 
357
  res.sendStatus(200);
358
  });
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  server.listen(PORT, "0.0.0.0", () => {
361
  log("STARTUP", `Telecom Server running on port ${PORT}`);
362
  });