/** Helpers that emit Twilio Markup Language XML responses. */ function xmlEscape(s: string): string { return s.replace(/[<>&"']/g, (c) => { switch (c) { case "<": return "<"; case ">": return ">"; case "&": return "&"; case '"': return """; case "'": return "'"; default: return c; } }); } /** * TwiML that opens a bidirectional Media Stream to our WebSocket endpoint. * Twilio forwards `customParams` as `customParameters` in the WS `start` event, * which we use to attach the call's pre-built context. */ export function streamTwiML(wssUrl: string, customParams: Record): string { const params = Object.entries(customParams) .map(([k, v]) => ` `) .join("\n"); return [ ``, ``, ` `, ` `, params, ` `, ` `, ``, ] .filter((line) => line.length > 0) .join("\n"); } /** Speak a short message and hang up — used when realtime isn't available. */ export function sayAndHangupTwiML(text: string): string { return [ ``, ``, ` ${xmlEscape(text)}`, ` `, ``, ].join("\n"); } /** Reject the call (Twilio counts this as not-answered — lower cost than answering and hanging up). */ export function rejectTwiML(): string { return [``, ``, ` `, ``].join("\n"); }