diff --git a/twilio-voice-app/.env.example b/twilio-voice-app/.env.example index 0a2e8f3..1b71217 100644 --- a/twilio-voice-app/.env.example +++ b/twilio-voice-app/.env.example @@ -3,10 +3,6 @@ TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX TWILIO_AUTH_TOKEN=your_auth_token TWILIO_PHONE_NUMBER=+1234567890 -# Publicly accessible URL (https://...) that Twilio can reach. -# When running locally use a tunneling tool such as ngrok and paste the HTTPS URL. -PUBLIC_SERVER_URL=https://your-ngrok-id.ngrok-free.app - # Optional: choose a Pollinations voice for TTS playback POLLINATIONS_VOICE=nova diff --git a/twilio-voice-app/README.md b/twilio-voice-app/README.md index 7d9fe3a..c4c7835 100644 --- a/twilio-voice-app/README.md +++ b/twilio-voice-app/README.md @@ -29,7 +29,6 @@ A lightweight Node + Twilio companion service that lets Unity call a phone numbe ``` Required variables: - `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER` - - `PUBLIC_SERVER_URL` – The **public** HTTPS URL that Twilio will call back (e.g. your ngrok tunnel). - Optional: `POLLINATIONS_VOICE` to choose a different Pollinations voice preset. 3. Start the server: ```bash @@ -40,10 +39,10 @@ A lightweight Node + Twilio companion service that lets Unity call a phone numbe # Example using ngrok (server must already be running on PORT) ngrok http 4000 ``` -5. Update `PUBLIC_SERVER_URL` in your `.env` with the HTTPS forwarding address printed by ngrok and restart the server if needed. - Visit [http://localhost:4000](http://localhost:4000) to load the dashboard, enter a phone number, and press **Call My Phone**. Answer the incoming call from your Twilio number to begin the voice chat. +> ℹ️ The server automatically infers its public URL from the incoming browser request when you press **Call My Phone**. Ensure you access the dashboard through the same public HTTPS address (for example, the ngrok URL) so Twilio receives a reachable callback URL. + ## Twilio configuration tips - Trial accounts can only call verified numbers. Add your mobile phone to the **Verified Caller IDs** page in the Twilio Console. diff --git a/twilio-voice-app/server.js b/twilio-voice-app/server.js index fc42295..a50a264 100644 --- a/twilio-voice-app/server.js +++ b/twilio-voice-app/server.js @@ -12,7 +12,6 @@ if (!fetchImpl) { } const PORT = process.env.PORT || 4000; -const PUBLIC_SERVER_URL = process.env.PUBLIC_SERVER_URL; const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID; const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN; const TWILIO_PHONE_NUMBER = process.env.TWILIO_PHONE_NUMBER; @@ -24,11 +23,8 @@ const hasTwilioCredentials = if (!hasTwilioCredentials) { console.warn('[WARN] Twilio credentials are not fully configured. API routes will return errors.'); } -if (!PUBLIC_SERVER_URL) { - console.warn('[WARN] PUBLIC_SERVER_URL is not set. Twilio callbacks will fail without a public URL.'); -} - const app = express(); +app.set('trust proxy', true); app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -146,15 +142,15 @@ function buildVoiceResponse(session, twiml, promptMessage, gatherPrompt) { return twiml; } -async function startPhoneCall(session) { +async function startPhoneCall(session, baseUrl) { if (!client) { throw new Error('Twilio client is not configured.'); } - if (!PUBLIC_SERVER_URL) { - throw new Error('PUBLIC_SERVER_URL is not configured.'); + if (!baseUrl) { + throw new Error('Unable to determine a public server URL from the request.'); } - const voiceUrl = new URL('/voice-response', PUBLIC_SERVER_URL); + const voiceUrl = new URL('/voice-response', baseUrl); voiceUrl.searchParams.set('sessionId', session.id); return client.calls.create({ @@ -165,6 +161,15 @@ async function startPhoneCall(session) { }); } +function resolvePublicBaseUrl(req) { + const forwardedProto = req.headers['x-forwarded-proto']; + const forwardedHost = req.headers['x-forwarded-host']; + const protocol = forwardedProto ? forwardedProto.split(',')[0] : req.protocol; + const host = forwardedHost ? forwardedHost.split(',')[0] : req.get('host'); + if (!protocol || !host) return null; + return `${protocol}://${host}`; +} + app.post('/api/start-call', async (req, res) => { try { const { phoneNumber, initialPrompt, voice } = req.body || {}; @@ -174,8 +179,12 @@ app.post('/api/start-call', async (req, res) => { if (!client) { return res.status(500).json({ error: 'Twilio credentials are missing on the server.' }); } - if (!PUBLIC_SERVER_URL) { - return res.status(500).json({ error: 'PUBLIC_SERVER_URL is not configured on the server.' }); + + const baseUrl = resolvePublicBaseUrl(req); + if (!baseUrl) { + return res.status(500).json({ + error: 'Unable to infer the public URL from the request. Access the app via a public HTTPS address before starting a call.' + }); } const session = createSession(phoneNumber.trim(), voice || DEFAULT_VOICE); @@ -187,7 +196,7 @@ app.post('/api/start-call', async (req, res) => { await fetchPollinationsResponse(session, 'Greet the caller briefly and ask how you can help.'); } - await startPhoneCall(session); + await startPhoneCall(session, baseUrl); res.json({ sessionId: session.id,