Pepguy commited on
Commit
7eaef40
Β·
verified Β·
1 Parent(s): 1c093c7

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +123 -32
app.js CHANGED
@@ -1,10 +1,13 @@
1
  import express from "express";
2
  import { createServer } from "http";
 
3
  import { randomUUID } from "crypto";
4
 
5
  const app = express();
6
  const server = createServer(app);
 
7
 
 
8
  app.use(express.static("public", { index: false }));
9
  app.use(express.json());
10
  app.use(express.urlencoded({ extended: true }));
@@ -14,7 +17,10 @@ const TELNYX_API_KEY = process.env.TELNYX_API_KEY;
14
  const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
15
  const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER;
16
 
17
- const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}]`, ...args);
 
 
 
18
 
19
  // ─── IN-MEMORY DB (Strictly for this Demo) ───
20
  let callLogs = [];
@@ -32,8 +38,8 @@ app.get("/", (req, res) => {
32
  <title>Telecom Layer Demo</title>
33
  <script src="https://cdn.tailwindcss.com"></script>
34
  <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
35
- <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
36
- <script src="https://unpkg.com/@babel/standalone@7.23.10/babel.min.js"></script>
37
  </head>
38
  <body class="bg-gray-900 text-gray-100 p-8 font-sans">
39
  <div id="root" class="max-w-4xl mx-auto"></div>
@@ -65,42 +71,42 @@ app.get("/", (req, res) => {
65
  };
66
 
67
  return (
68
- <div class="space-y-8">
69
- <h1 class="text-3xl font-bold border-b border-gray-700 pb-4">Telecom Layer & Ambience Test</h1>
70
 
71
- <div class="bg-gray-800 p-6 rounded-lg shadow-lg flex gap-4 items-end">
72
- <div class="flex-1">
73
- <label class="block text-sm text-gray-400 mb-1">Target Phone Number</label>
74
- <input type="text" value={targetNumber} onChange={e => setTargetNumber(e.target.value)} class="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white" />
75
  </div>
76
- <button onClick={dial} class="bg-blue-600 hover:bg-blue-500 px-8 py-2 rounded font-bold transition">Test Call</button>
77
  </div>
78
- {status && <div class="text-sm text-yellow-400">{status}</div>}
79
 
80
- <div class="bg-gray-800 rounded-lg overflow-hidden shadow-lg">
81
- <table class="w-full text-left">
82
- <thead class="bg-gray-900 text-gray-400 text-sm">
83
- <tr><th class="p-4">Call ID</th><th class="p-4">Number</th><th class="p-4">Status</th><th class="p-4">Recording</th></tr>
84
  </thead>
85
- <tbody class="divide-y divide-gray-700">
86
  {logs.map(log => (
87
  <tr key={log.id}>
88
- <td class="p-4 text-xs font-mono text-gray-500">{log.call_control_id.slice(0,8)}...</td>
89
- <td class="p-4 font-mono">{log.target_number}</td>
90
- <td class="p-4">
91
- <span class={\`px-2 py-1 rounded text-xs \${log.status === 'completed' ? 'bg-green-900 text-green-300' : 'bg-yellow-900 text-yellow-300'}\`}>
92
  {log.status}
93
  </span>
94
  </td>
95
- <td class="p-4">
96
  {log.recording_url ?
97
- <audio controls src={log.recording_url} class="h-8 w-48"></audio>
98
- : <span class="text-gray-500 text-sm">Waiting for hangup...</span>
99
  }
100
  </td>
101
  </tr>
102
  ))}
103
- {logs.length === 0 && <tr><td colSpan="4" class="p-4 text-center text-gray-500">No calls yet</td></tr>}
104
  </tbody>
105
  </table>
106
  </div>
@@ -115,26 +121,37 @@ app.get("/", (req, res) => {
115
  });
116
 
117
  app.get("/api/logs", (req, res) => {
118
- // Return logs newest first
119
  res.json([...callLogs].reverse());
120
  });
121
 
122
  // ─────────────────────────────────────────────
123
- // 2. PROXY FOR TELNYX RECORDINGS
124
  // ─────────────────────────────────────────────
125
- // Telnyx recording URLs require your API key to download.
126
- // This proxy lets the HTML <audio> tag play it securely without exposing your key.
127
  app.get("/play-recording/:call_control_id", async (req, res) => {
128
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
129
- if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
 
 
 
130
 
131
  try {
132
  const telnyxRes = await fetch(logEntry.raw_telnyx_url, {
133
  headers: { Authorization: `Bearer ${TELNYX_API_KEY}` }
134
  });
 
 
 
 
 
 
 
135
  res.setHeader("Content-Type", "audio/mpeg");
136
- telnyxRes.body.pipe(res);
 
 
 
137
  } catch (e) {
 
138
  res.sendStatus(500);
139
  }
140
  });
@@ -208,7 +225,7 @@ app.post("/webhook/call", async (req, res) => {
208
  // 1. START DUAL-CHANNEL RECORDING
209
  await telnyxCmd("record_start", {
210
  format: "mp3",
211
- channels: "dual", // Stereo separation for easier listening
212
  play_beep: false
213
  });
214
 
@@ -231,7 +248,6 @@ app.post("/webhook/call", async (req, res) => {
231
  const rawUrl = payload.recording_urls.mp3;
232
  log("RECORDING", `Saved. URL: ${rawUrl}`);
233
 
234
- // Set the proxy URL so the dashboard can play it securely
235
  updateLog({
236
  status: "completed",
237
  raw_telnyx_url: rawUrl,
@@ -246,6 +262,81 @@ app.post("/webhook/call", async (req, res) => {
246
  res.sendStatus(200);
247
  });
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  server.listen(PORT, "0.0.0.0", () => {
250
  log("STARTUP", `Telecom Demo Server running on port ${PORT}`);
251
  });
 
1
  import express from "express";
2
  import { createServer } from "http";
3
+ import { WebSocketServer } from "ws";
4
  import { randomUUID } from "crypto";
5
 
6
  const app = express();
7
  const server = createServer(app);
8
+ const wss = new WebSocketServer({ server, path: "/audio-stream" });
9
 
10
+ // ── FIX: Disable default index.html serving so it doesn't shadow our dashboard ──
11
  app.use(express.static("public", { index: false }));
12
  app.use(express.json());
13
  app.use(express.urlencoded({ extended: true }));
 
17
  const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
18
  const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER;
19
 
20
+ const log = (tag, ...args) => {
21
+ const time = new Date().toISOString();
22
+ console.log(`[${time}] [${tag}]`, ...args);
23
+ };
24
 
25
  // ─── IN-MEMORY DB (Strictly for this Demo) ───
26
  let callLogs = [];
 
38
  <title>Telecom Layer Demo</title>
39
  <script src="https://cdn.tailwindcss.com"></script>
40
  <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
41
+ <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
42
+ <script src="https://unpkg.com/@babel/standalone@7.23.10/babel.min.js"></script>
43
  </head>
44
  <body class="bg-gray-900 text-gray-100 p-8 font-sans">
45
  <div id="root" class="max-w-4xl mx-auto"></div>
 
71
  };
72
 
73
  return (
74
+ <div className="space-y-8">
75
+ <h1 className="text-3xl font-bold border-b border-gray-700 pb-4">Telecom Layer & Ambience Test</h1>
76
 
77
+ <div className="bg-gray-800 p-6 rounded-lg shadow-lg flex gap-4 items-end">
78
+ <div className="flex-1">
79
+ <label className="block text-sm text-gray-400 mb-1">Target Phone Number</label>
80
+ <input type="text" value={targetNumber} onChange={e => setTargetNumber(e.target.value)} className="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white outline-none focus:border-blue-500" />
81
  </div>
82
+ <button onClick={dial} className="bg-blue-600 hover:bg-blue-500 px-8 py-2 rounded font-bold transition">Test Call</button>
83
  </div>
84
+ {status && <div className="text-sm text-yellow-400">{status}</div>}
85
 
86
+ <div className="bg-gray-800 rounded-lg overflow-hidden shadow-lg">
87
+ <table className="w-full text-left">
88
+ <thead className="bg-gray-900 text-gray-400 text-sm">
89
+ <tr><th className="p-4">Call ID</th><th className="p-4">Number</th><th className="p-4">Status</th><th className="p-4">Recording</th></tr>
90
  </thead>
91
+ <tbody className="divide-y divide-gray-700">
92
  {logs.map(log => (
93
  <tr key={log.id}>
94
+ <td className="p-4 text-xs font-mono text-gray-500">{log.call_control_id.slice(0,8)}...</td>
95
+ <td className="p-4 font-mono">{log.target_number}</td>
96
+ <td className="p-4">
97
+ <span className={\`px-2 py-1 rounded text-xs \${log.status === 'completed' ? 'bg-green-900 text-green-300' : 'bg-yellow-900 text-yellow-300'}\`}>
98
  {log.status}
99
  </span>
100
  </td>
101
+ <td className="p-4">
102
  {log.recording_url ?
103
+ <audio controls src={log.recording_url} className="h-8 w-48"></audio>
104
+ : <span className="text-gray-500 text-sm">Waiting for hangup...</span>
105
  }
106
  </td>
107
  </tr>
108
  ))}
109
+ {logs.length === 0 && <tr><td colSpan="4" className="p-4 text-center text-gray-500">No calls yet</td></tr>}
110
  </tbody>
111
  </table>
112
  </div>
 
121
  });
122
 
123
  app.get("/api/logs", (req, res) => {
 
124
  res.json([...callLogs].reverse());
125
  });
126
 
127
  // ─────────────────────────────────────────────
128
+ // 2. PROXY FOR TELNYX RECORDINGS (FIXED BUFFER)
129
  // ─────────────────────────────────────────────
 
 
130
  app.get("/play-recording/:call_control_id", async (req, res) => {
131
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
132
+ if (!logEntry || !logEntry.raw_telnyx_url) {
133
+ log("PROXY", "Recording URL not found in memory");
134
+ return res.sendStatus(404);
135
+ }
136
 
137
  try {
138
  const telnyxRes = await fetch(logEntry.raw_telnyx_url, {
139
  headers: { Authorization: `Bearer ${TELNYX_API_KEY}` }
140
  });
141
+
142
+ if (!telnyxRes.ok) throw new Error(`Telnyx returned ${telnyxRes.status}`);
143
+
144
+ // Buffer the audio download to bypass Node.js stream piping bugs
145
+ const arrayBuffer = await telnyxRes.arrayBuffer();
146
+ const buffer = Buffer.from(arrayBuffer);
147
+
148
  res.setHeader("Content-Type", "audio/mpeg");
149
+ res.setHeader("Content-Length", buffer.length);
150
+ res.send(buffer);
151
+
152
+ log("PROXY", `Served audio for ${req.params.call_control_id} (${buffer.length} bytes)`);
153
  } catch (e) {
154
+ log("PROXY ERROR", e.message);
155
  res.sendStatus(500);
156
  }
157
  });
 
225
  // 1. START DUAL-CHANNEL RECORDING
226
  await telnyxCmd("record_start", {
227
  format: "mp3",
228
+ channels: "dual",
229
  play_beep: false
230
  });
231
 
 
248
  const rawUrl = payload.recording_urls.mp3;
249
  log("RECORDING", `Saved. URL: ${rawUrl}`);
250
 
 
251
  updateLog({
252
  status: "completed",
253
  raw_telnyx_url: rawUrl,
 
262
  res.sendStatus(200);
263
  });
264
 
265
+ // ─────────────────────────────────────────────
266
+ // 5. TELNYX WEBHOOK (SMS)
267
+ // ─────────────────────────────────────────────
268
+ app.post("/webhook/sms", (req, res) => {
269
+ const event = req.body?.data;
270
+ const type = event?.event_type;
271
+ const payload = event?.payload;
272
+
273
+ log("SMS EVENT", `type=${type}`);
274
+
275
+ if (type === "message.received") {
276
+ const from = payload?.from?.phone_number;
277
+ const text = payload?.text;
278
+ log("SMS INBOUND", `from=${from} text="${text}"`);
279
+ }
280
+
281
+ if (type === "message.finalized") {
282
+ const status = payload?.to?.[0]?.status;
283
+ const to = payload?.to?.[0]?.phone_number;
284
+ log("SMS STATUS", `to=${to} status=${status}`);
285
+ }
286
+
287
+ res.sendStatus(200);
288
+ });
289
+
290
+ // ─────────────────────────────────────────────
291
+ // WEBSOCKET β€” audio stream
292
+ // ─────────────────────────────────────────────
293
+ wss.on("connection", (ws, req) => {
294
+ log("WS", `Client connected from ${req.socket.remoteAddress}`);
295
+
296
+ ws.on("message", (raw) => {
297
+ try {
298
+ const msg = JSON.parse(raw);
299
+
300
+ switch (msg.event) {
301
+ case "connected":
302
+ log("WS", `Stream connected protocol=${msg.protocol} version=${msg.version}`);
303
+ break;
304
+
305
+ case "start":
306
+ log("WS START", `stream_id=${msg.start?.stream_id} call_control_id=${msg.start?.call_control_id} tracks=${JSON.stringify(msg.start?.tracks)}`);
307
+ break;
308
+
309
+ case "media":
310
+ break;
311
+
312
+ case "stop":
313
+ log("WS STOP", `stream_id=${msg.stop?.stream_id} reason=${msg.stop?.reason}`);
314
+ break;
315
+
316
+ default:
317
+ log("WS UNKNOWN EVENT", JSON.stringify(msg));
318
+ }
319
+ } catch (err) {
320
+ log("WS PARSE ERROR", err.message);
321
+ }
322
+ });
323
+
324
+ ws.on("close", (code, reason) => log("WS", `Client disconnected code=${code} reason=${reason}`));
325
+ ws.on("error", (err) => log("WS ERROR", err.message));
326
+ });
327
+
328
+ // ─────────────────────────────────────────────
329
+ // STARTUP CONFIGURATION
330
+ // ─────────────────────────────────────────────
331
+ const requiredEnv = ["TELNYX_API_KEY", "TELNYX_CONNECTION_ID", "TELNYX_FROM_NUMBER"];
332
+ requiredEnv.forEach((key) => {
333
+ if (!process.env[key]) {
334
+ log("STARTUP WARNING", `Missing env var: ${key}`);
335
+ } else {
336
+ log("STARTUP", `${key} is set βœ“`);
337
+ }
338
+ });
339
+
340
  server.listen(PORT, "0.0.0.0", () => {
341
  log("STARTUP", `Telecom Demo Server running on port ${PORT}`);
342
  });