Pepguy commited on
Commit
8dfe5dc
·
verified ·
1 Parent(s): 986106c

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +9 -333
app.js CHANGED
@@ -1,330 +1,3 @@
1
- /* import express from "express";
2
- import { createServer } from "http";
3
- import { WebSocketServer } from "ws";
4
-
5
- const app = express();
6
- const server = createServer(app);
7
- const wss = new WebSocketServer({ server, path: "/audio-stream" });
8
-
9
- app.use(express.static("public", { index: false }));
10
- app.use(express.json());
11
-
12
- const PORT = process.env.PORT || 7860;
13
- 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
- const AI_SERVER_URL = process.env.AI_SERVER_URL;
17
- const TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER;
18
- const TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID;
19
-
20
- const ACTIVE_VOICE = "Enceladus_3";
21
- const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}]`, ...args);
22
-
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);
30
- for (let i = 0; i < 256; i++) {
31
- const sign = (i & 0x80) ? -1 : 1;
32
- const exponent = (~i >> 4) & 0x07;
33
- const mantissa = ~i & 0x0f;
34
- const sample = ((mantissa << 3) + 132) << exponent;
35
- MU_LAW_TABLE[i] = sign * (sample - 132);
36
- }
37
- function decodeMuLaw(buffer) {
38
- const out = new Int16Array(buffer.length);
39
- for (let i = 0; i < buffer.length; i++) out[i] = MU_LAW_TABLE[buffer[i]];
40
- return out;
41
- }
42
- function calculateRMS(pcm) {
43
- let sum = 0;
44
- for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i];
45
- return Math.sqrt(sum / pcm.length);
46
- }
47
-
48
- const sendTelnyxCmd = async (callId, action, body = {}) => {
49
- try {
50
- const url = callId ? `https://api.telnyx.com/v2/calls/${callId}/actions/${action}` : `https://api.telnyx.com/v2/calls`;
51
- const response = await fetch(url, {
52
- method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
53
- body: JSON.stringify(body)
54
- });
55
- if (!response.ok) log("TELNYX CMD ERROR", `[${action}] ${await response.text()}`);
56
- return response;
57
- } catch (e) { log("TELNYX CMD NETWORK ERROR", e.message); }
58
- };
59
-
60
- app.post("/api/call-action", async (req, res) => {
61
- const { call_control_id, action, lead_id } = req.body;
62
- if (!pendingCalls.has(call_control_id)) return res.status(404).json({ error: "Call not pending" });
63
-
64
- if (lead_id) activeLeadIds.set(call_control_id, lead_id);
65
- pendingCalls.set(call_control_id, action);
66
-
67
- if (action === "reject") {
68
- await sendTelnyxCmd(call_control_id, "hangup");
69
- pendingCalls.delete(call_control_id);
70
- } else {
71
- await sendTelnyxCmd(call_control_id, "answer");
72
- }
73
- res.json({ ok: true });
74
- });
75
-
76
- app.post("/api/hangup", async (req, res) => {
77
- await sendTelnyxCmd(req.body.call_control_id, "hangup");
78
- res.json({ ok: true });
79
- });
80
-
81
- app.post("/api/send-sms", async (req, res) => {
82
- log("SMS", `Attempting outbound SMS to ${req.body.to}...`);
83
- try {
84
- const sendRequest = async (fromNumber) => {
85
- const payload = { from: fromNumber, to: req.body.to, text: req.body.text };
86
- if (TELNYX_MESSAGING_PROFILE_ID) payload.messaging_profile_id = TELNYX_MESSAGING_PROFILE_ID;
87
-
88
- return await fetch("https://api.telnyx.com/v2/messages", {
89
- method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
90
- body: JSON.stringify(payload)
91
- });
92
- };
93
-
94
- let response = await sendRequest(TELNYX_ALPHA_SENDER);
95
- let data = await response.json();
96
-
97
- if (!response.ok && data.errors && data.errors.some(e => e.code === '40305' || e.code === '40012')) {
98
- log("SMS WARNING", `Alpha Sender rejected. Falling back to standard US number ${TELNYX_FROM_NUMBER}...`);
99
- response = await sendRequest(TELNYX_FROM_NUMBER);
100
- data = await response.json();
101
- }
102
-
103
- if (!response.ok) log("SMS ERROR", JSON.stringify(data));
104
- else log("SMS SUCCESS", `Message accepted by Telnyx for delivery to ${req.body.to}`);
105
-
106
- res.json(data);
107
- } catch(e) {
108
- log("SMS NETWORK ERROR", e.message);
109
- res.status(500).json({ error: e.message });
110
- }
111
- });
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`, {
147
- method: "POST", headers: { "Content-Type": "application/json" },
148
- body: JSON.stringify({ call_control_id: ccid, action, lead_id })
149
- }).catch(() => {});
150
-
151
- res.json({ ok: true, call_control_id: ccid });
152
- } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
153
- });
154
-
155
- app.post("/webhook/call", async (req, res) => {
156
- res.sendStatus(200);
157
- const type = req.body?.data?.event_type;
158
- const payload = req.body?.data?.payload;
159
- const callId = payload?.call_control_id;
160
- if (!type) return;
161
-
162
- switch (type) {
163
- case "call.initiated":
164
- if (payload?.direction === "incoming") {
165
- const fromNumber = payload.from;
166
- const now = Date.now();
167
-
168
- if (incomingCallDebounce.has(fromNumber)) {
169
- if (now - incomingCallDebounce.get(fromNumber) < 10000) {
170
- log("INCOMING DEBOUNCED", `Ignoring duplicate payload from ${fromNumber}`);
171
- return;
172
- }
173
- }
174
- incomingCallDebounce.set(fromNumber, now);
175
-
176
- log("INCOMING", `From: ${fromNumber}`);
177
- pendingCalls.set(callId, "waiting");
178
-
179
- await fetch(`${AI_SERVER_URL}/api/incoming-call`, {
180
- method: "POST", headers: { "Content-Type": "application/json" },
181
- body: JSON.stringify({ call_control_id: callId, from: fromNumber })
182
- }).catch(()=>{});
183
- }
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
- }
199
-
200
- const actionType = pendingCalls.get(callId) || "ai";
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" });
228
- await sendTelnyxCmd(callId, "transcription_start", { transcription_engine: "Telnyx", transcription_tracks: "inbound" });
229
- setTimeout(async () => { log("HANGUP", `Enforcing 90s limit on ${callId}`); await sendTelnyxCmd(callId, "hangup"); }, 90000);
230
- }
231
- break;
232
-
233
- case "call.transcription":
234
- if (!payload?.transcription_data?.is_final) break;
235
- try {
236
- const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
237
- method: "POST", headers: { "Content-Type": "application/json" },
238
- body: JSON.stringify({ text: payload.transcription_data.transcript.trim(), call_id: callId, lead_id: activeLeadIds.get(callId) || null })
239
- });
240
- if (aiRes.ok) {
241
- const aiData = await aiRes.json();
242
- if (aiData.speak_url) await sendTelnyxCmd(callId, "playback_start", { audio_url: aiData.speak_url, target_legs: "self" });
243
- }
244
- } catch (e) { log("AI_SERVER_ERROR", e.message); }
245
- break;
246
-
247
- case "call.recording.saved":
248
- if (payload.recording_urls?.mp3) {
249
- await fetch(`${AI_SERVER_URL}/api/save-recording`, {
250
- method: "POST", headers: { "Content-Type": "application/json" },
251
- body: JSON.stringify({ call_control_id: callId, recording_url: payload.recording_urls.mp3 })
252
- })
253
- .then(async (r) => { if (!r.ok) log("RECORDING ERROR", `AI Server status: ${r.status}`); })
254
- .catch((err) => log("RECORDING NET ERROR", err.message));
255
- }
256
- break;
257
-
258
- case "call.hangup":
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 })
266
- }).catch(() => {});
267
- break;
268
- }
269
- });
270
-
271
- wss.on("connection", (ws) => {
272
- let isSpeaking = false;
273
- let silenceFrames = 0;
274
- let callControlId = null;
275
- let hasPlayedFiller = false;
276
- let hasGreeted = false;
277
- let totalNoiseFrames = 0;
278
-
279
- ws.on("message", async (raw) => {
280
- try {
281
- const msg = JSON.parse(raw);
282
- if (msg.event === "start") {
283
- callControlId = msg.start?.call_control_id;
284
- setTimeout(async () => {
285
- if (hasGreeted || !callControlId) return;
286
- hasGreeted = true;
287
- const greetingPhrase = "Hi, this is Nathan.";
288
- await sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(greetingPhrase)}`, target_legs: "self" });
289
- 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(() => {});
290
- }, 1500);
291
- }
292
-
293
- if (msg.event === "media") {
294
- const pcm16 = decodeMuLaw(Buffer.from(msg.media.payload, "base64"));
295
- const rms = calculateRMS(pcm16);
296
-
297
- if (rms > 500) {
298
- hasGreeted = true;
299
- silenceFrames = 0;
300
- totalNoiseFrames++;
301
- if (!isSpeaking) {
302
- isSpeaking = true;
303
- hasPlayedFiller = false;
304
- await sendTelnyxCmd(callControlId, "playback_stop");
305
- }
306
- } else if (isSpeaking) {
307
- silenceFrames++;
308
- if (silenceFrames > 35 && !hasPlayedFiller) {
309
- hasPlayedFiller = true;
310
- isSpeaking = false;
311
- if (totalNoiseFrames > 40) {
312
- const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
313
- 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(() => {});
314
- }
315
- totalNoiseFrames = 0;
316
- }
317
- }
318
- }
319
- } catch {}
320
- });
321
- });
322
-
323
- server.listen(PORT, "0.0.0.0", () => {
324
- log("STARTUP", `Telecom routing server running on port ${PORT}`);
325
- });
326
- */
327
-
328
  import express from "express";
329
  import { createServer } from "http";
330
  import { WebSocketServer } from "ws";
@@ -341,7 +14,6 @@ const TELNYX_API_KEY = process.env.TELNYX_API_KEY;
341
  const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
342
  const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER;
343
  const AI_SERVER_URL = process.env.AI_SERVER_URL;
344
- const SIP_USERNAME = process.env.TELNYX_SIP_USERNAME;
345
  const TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER;
346
  const TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID;
347
 
@@ -495,9 +167,12 @@ app.post("/webhook/call", async (req, res) => {
495
  case "call.answered":
496
  if (payload?.client_state) {
497
  const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
498
- log("BRIDGING", `Agent picked up via WebRTC! Stopping ringback and Bridging Leg A (${legACallId}) ↔ Leg B (${callId})`);
499
 
 
500
  await sendTelnyxCmd(legACallId, "playback_stop");
 
 
501
  await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
502
  break;
503
  }
@@ -507,17 +182,18 @@ app.post("/webhook/call", async (req, res) => {
507
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
508
 
509
  if (actionType === "manual") {
510
- log("BRIDGING", `Dialing Telnyx WebRTC client for manual takeover...`);
511
 
512
- // Play ringing to the customer while they wait for you
513
  await sendTelnyxCmd(callId, "playback_start", {
514
  audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav",
515
  target_legs: "self"
516
  });
517
 
518
- // Dial your SIP Username
 
519
  await sendTelnyxCmd(null, "dial", {
520
- to: `sip:${SIP_USERNAME}@sip.telnyx.com`,
521
  from: TELNYX_FROM_NUMBER,
522
  connection_id: TELNYX_CONNECTION_ID,
523
  client_state: Buffer.from(callId).toString("base64")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import express from "express";
2
  import { createServer } from "http";
3
  import { WebSocketServer } from "ws";
 
14
  const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
15
  const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER;
16
  const AI_SERVER_URL = process.env.AI_SERVER_URL;
 
17
  const TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER;
18
  const TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID;
19
 
 
167
  case "call.answered":
168
  if (payload?.client_state) {
169
  const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
170
+ log("BRIDGING", `Agent picked up! Stopping ringback and Bridging Leg A (${legACallId}) ↔ Leg B (${callId})`);
171
 
172
+ // Stop the ringing sound for the customer
173
  await sendTelnyxCmd(legACallId, "playback_stop");
174
+
175
+ // Bridge them
176
  await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
177
  break;
178
  }
 
182
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
183
 
184
  if (actionType === "manual") {
185
+ log("BRIDGING", `Dialing Agent Cell (+2348069234734) for manual takeover...`);
186
 
187
+ // 1. Play ringing to the customer
188
  await sendTelnyxCmd(callId, "playback_start", {
189
  audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav",
190
  target_legs: "self"
191
  });
192
 
193
+ // 2. Dial your cell phone
194
+ const MY_CELL_NUMBER = "+2348069234734";
195
  await sendTelnyxCmd(null, "dial", {
196
+ to: MY_CELL_NUMBER,
197
  from: TELNYX_FROM_NUMBER,
198
  connection_id: TELNYX_CONNECTION_ID,
199
  client_state: Buffer.from(callId).toString("base64")