// CallSlayer v3 — Voice + SMS capture with Two-Tier Enrichment + Deterministic Defendant tracking // Node >= 20; package.json deps: "express", "twilio", "openai" import express from "express"; import path from "path"; import { fileURLToPath } from "url"; import crypto from "node:crypto"; import twilio from "twilio"; import OpenAI from "openai"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // ===== Env ===== const PUBLIC_BASE_URL = (process.env.PUBLIC_BASE_URL || "").trim().replace(/\/$/, ""); const GREETING_TTS = (process.env.GREETING_TTS || "The wireless customer you are calling is not available at this time. Please leave your message after the tone.").trim(); const GREETING_VOICE = (process.env.GREETING_VOICE || "Polly.Joanna").trim(); const GOOGLE_INTAKE_EXEC = process.env.GOOGLE_INTAKE_EXEC || process.env.GOOGLE_INTAKE_WEBHOOK || ""; const TEST_MODE = String(process.env.TEST_MODE || "0") === "1"; const AI_PARSE_ON = String(process.env.AI_PARSE_ON || "1") === "1"; // Whisper on voice const AI_WEB_ENRICH_ON = String(process.env.AI_WEB_ENRICH_ON || "0") === "1";// legacy flag (kept) const LOOKUP_ON = String(process.env.LOOKUP_ON || "0") === "1"; // unused here const INJECT_2 = String(process.env.INJECT_2 || "0") === "1"; const INJECT_2_DELAY_MS = Number(process.env.INJECT_2_DELAY_MS || 3500); // AI + Web Hunt const AI_EXTRACT_ON = String(process.env.AI_EXTRACT_ON || "1") === "1"; const AI_EXTRACT_MODEL = (process.env.AI_EXTRACT_MODEL || "gpt-4o-mini").trim(); const OPEN_WEB_HUNT_ON = String(process.env.OPEN_WEB_HUNT_ON || "1") === "1"; const HUNT_TIMEOUT_MS = Number(process.env.HUNT_TIMEOUT_MS || 5000); const MAX_FETCH_BYTES = Number(process.env.MAX_FETCH_BYTES || 12000); const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; const TWILIO_ACCOUNT_SID = process.env.TWILIO_ACCOUNT_SID || ""; const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN || ""; const PORT = Number(process.env.PORT || 3000); // ===== App ===== const app = express(); app.use(express.urlencoded({ extended: true })); // Twilio x-www-form-urlencoded app.use(express.json()); app.use(express.static(path.join(__dirname, "..", "public"))); // static assets // ===== Tail logs ===== const TAIL_MAX = 500; const tail = []; function logTail(line){ const ts = new Date().toISOString(); const msg = `[${ts}] ${line}`; tail.push(msg); if (tail.length > TAIL_MAX) tail.shift(); console.log(msg); } // ===== Utils ===== function xml(res, body){ res.set("Content-Type","text/xml; charset=utf-8").send(body); } function withTimeout(promise, ms, label = "op") { return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); promise.then(v => { clearTimeout(t); resolve(v); }) .catch(e => { clearTimeout(t); reject(e); }); }); } // File polyfill for Whisper (Node 20 fetch supports Blob) if (typeof File === "undefined") { globalThis.File = class extends Blob { constructor(parts, name, opts = {}) { super(parts, opts); this.name = name; this.lastModified = Date.now(); } }; } async function fetchWithTimeout(url, opts = {}, ms = 4000) { const ctrl = new AbortController(); const id = setTimeout(() => ctrl.abort(), ms); try { return await fetch(url, { ...opts, signal: ctrl.signal, headers: { "User-Agent": "CallSlayer/3.0 (+https://calls)", ...(opts.headers||{}) } }); } finally { clearTimeout(id); } } function textClamp(s){ if (!s) return ""; s = s.replace(/\s+/g, " ").trim(); return s.length > MAX_FETCH_BYTES ? (s.slice(0, MAX_FETCH_BYTES) + " …") : s; } function humanTimestampET(){ const s = new Date().toLocaleString("en-US", { timeZone: "America/New_York", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: true }); return `${s} ET`; } // ===== Parsing helpers ===== function normalizeSpokenEmail(raw){ if (!raw) return ""; let s = raw.toLowerCase(); s = s.replace(/\s+at\s+/g, "@").replace(/\s+dot\s+/g, ".").replace(/\s+/g, ""); s = s.replace(/['’]/g, "").replace(/[.,;:!?]+$/g, ""); return (/@/.test(s) && /\./.test(s)) ? s : ""; } function extractEmail(text){ if (!text) return ""; const m = text.match(/[A-Z0-9._%+'-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i); if (m) return m[0].toLowerCase(); const around = text.match(/email (?:me )?at ([a-z0-9 .'-]+?)(?:\s|$|\.|,)/i); return around ? normalizeSpokenEmail(around[1]) : ""; } function extractWebsite(text){ if (!text) return ""; const m = text.match(/(?:https?:\/\/)?(?:www\.)?([a-z0-9-]+\.[a-z.]{2,})(?:\/\S*)?/i); return m ? m[1].toLowerCase() : ""; } function getDomainFromEmail(email){ if (!email || !/@/.test(email)) return ""; return email.split("@")[1].toLowerCase(); } function normalizeDomain(domain){ return (domain||"").toLowerCase().replace(/^https?:\/\//,"").replace(/^www\./,"").replace(/\/.*/,""); } function inferCompanyFromDomain(domain){ if (!domain) return ""; const core = normalizeDomain(domain).split(".")[0]; return core.replace(/[-_]/g, " ").replace(/\b\w/g, c => c.toUpperCase()); } function capitalizeName(s){ return s.replace(/\b([a-z])/g, c => c.toUpperCase()).trim(); } function cleanCompany(s){ return s.replace(/[^\w\s&.-]/g, "").replace(/\s+/g, " ").trim(); } function extractContactAndCompany(text){ if (!text) return { contact_name: "", company_name: "" }; let m = text.match(/\b(?:this is|i(?:'| a)m)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(?:with|from)\s+([^.,]+)/i); if (m) return { contact_name: capitalizeName(m[1]), company_name: cleanCompany(m[2]) }; m = text.match(/\bwith\s+([^.,]+)/i); return { contact_name: "", company_name: m ? cleanCompany(m[1]) : "" }; } function onlyDigits(s){ return (s||"").replace(/[^\d]/g, ""); } // Callback numbers inside text → array of E.164 strings function extractCallbackNumbers(text){ if (!text) return []; const hits = []; const re = /(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}/g; const seen = new Set(); let m; while ((m = re.exec(text)) !== null) { const raw = m[0]; const d = onlyDigits(raw); if (d.length === 10) { const e164 = `+1${d}`; if (!seen.has(e164)) { seen.add(e164); hits.push(e164); } } else if (d.length === 11 && d.startsWith("1")) { const e164 = `+${d}`; if (!seen.has(e164)) { seen.add(e164); hits.push(e164); } } } return hits; } // Keyword flags + heuristic success score (conservative) function keywordHeuristics(text){ const t = (text||"").toLowerCase(); const flags = new Set(); let score = 0.1; const add = (f, s=0.05)=>{ flags.add(f); score += s; }; if (/debt consolidation|consolidation loan/.test(t)) add("debt_consolidation", 0.2); if (/\bloan\b/.test(t)) add("loan_offer", 0.1); if (/pre[-\s]?approval|pre[-\s]?approved/.test(t)) add("pre_approval", 0.1); if (/limited (?:time|offer|pre-?approval)/.test(t)) add("limited_time", 0.05); if (/call (?:me|now|back)|standing by|press \d/.test(t)) add("call_now", 0.05); if (/\b(?:833|844|855|866|877|888|800)\b/.test(t)) add("toll_free", 0.05); if (/monthly payment|per month|\$\s?\d+/.test(t)) add("payment_terms", 0.05); if (/message|voicemail|deposited a new message/.test(t)) add("voicemail_transcription", 0.05); score = Math.max(0, Math.min(1, score)); return { flags: Array.from(flags), success: score }; } // Simple robocall probability (rps) function computeRps({ flags = [], transcript = "", callback_number = "" }){ let s = 0.1; const t = (transcript||"").toLowerCase(); const set = new Set(flags); if (set.has("debt_consolidation")) s += 0.3; if (set.has("pre_approval")) s += 0.15; if (set.has("call_now")) s += 0.1; if (set.has("toll_free")) s += 0.1; if (/\b\d{2,}\s?(?:k|thousand|\$)/.test(t)) s += 0.1; if (/press \d|limited time|standing by/.test(t)) s += 0.1; if (/opt[- ]?out|stop to end/.test(t)) s -= 0.05; if (/unsubscribe|privacy|terms/.test(t)) s -= 0.05; const d = onlyDigits(callback_number); const first3 = (d.length === 11 && d.startsWith("1")) ? d.slice(1,4) : d.slice(0,3); if (/^(800|833|844|855|866|877|888)$/.test(first3)) s += 0.05; return Math.max(0, Math.min(1, s)); } // Deterministic defendant seed → id (website → callback# → email domain → company → from_number) function makeDefendantSeed({ website, callback_number, email, company_name, from_number }){ const emailDomain = getDomainFromEmail(email); const picks = [ normalizeDomain(website), onlyDigits(callback_number), normalizeDomain(emailDomain), (company_name||"").toLowerCase().replace(/\s+/g," ").trim(), onlyDigits(from_number) ].filter(Boolean); return picks[0] || "unknown"; } function makeDefendantIdFromSeed(seed){ const h = crypto.createHash("sha1").update(String(seed)).digest("hex").slice(0,24); return `D_${h}`; } // ===== Gating helpers ===== function hasEntityHints(text){ const t = (text||""); return !!(extractEmail(t) || extractWebsite(t) || extractCallbackNumbers(t).length || extractContactAndCompany(t).company_name); } function isTrivialText(text){ const t = (text||"").trim(); if (t.length < 20 && !hasEntityHints(t)) return true; return false; } // ===== Voice TwiML ===== function voiceTwiml({ recordingAction }) { const vr = new twilio.twiml.VoiceResponse(); vr.say({ voice: GREETING_VOICE }, GREETING_TTS); if (INJECT_2) { const pauseSec = Math.max(1, Math.min(60, Math.ceil(INJECT_2_DELAY_MS / 1000))); vr.pause({ length: pauseSec }); vr.play({ digits: "2" }); } vr.record({ action: recordingAction, method: "POST", playBeep: true, timeout: 6, maxLength: 180, trim: "do-not-trim" }); vr.say("Thank you. Goodbye."); return vr.toString(); } // ===== Twilio recording → MP3 buffer → Whisper ===== async function fetchTwilioRecordingBuffer(urlMp3){ if (!urlMp3) return null; if (!TWILIO_ACCOUNT_SID || !TWILIO_AUTH_TOKEN) { logTail("Transcribe skip: missing TWILIO creds"); return null; } const basic = Buffer.from(`${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}`).toString("base64"); const attempts = [0, 600, 1400]; for (let i = 0; i < attempts.length; i++) { if (i > 0) await new Promise(r => setTimeout(r, attempts[i])); const r = await fetchWithTimeout(urlMp3, { headers: { Authorization: `Basic ${basic}` } }, 7000); if (r.ok) { const ab = await r.arrayBuffer(); return Buffer.from(ab); } if (r.status !== 404 && r.status !== 409) { logTail(`Twilio MP3 fetch failed: ${r.status} ${r.statusText}`); return null; } logTail(`Twilio MP3 not ready (attempt ${i+1}/${attempts.length}): ${r.status}`); } logTail("Twilio MP3 fetch gave up after retries"); return null; } async function transcribeIfEnabled(mp3Url){ if (!AI_PARSE_ON) { logTail("Transcribe skip: AI_PARSE_ON=0"); return ""; } if (!OPENAI_API_KEY) { logTail("Transcribe skip: OPENAI_API_KEY missing"); return ""; } try{ const buf = await fetchTwilioRecordingBuffer(mp3Url); if (!buf) { logTail("Transcribe skip: no audio buffer"); return ""; } const file = new File([buf], "recording.mp3", { type: "audio/mpeg" }); const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); const result = await withTimeout( openai.audio.transcriptions.create({ file, model: "whisper-1" }), 14000, "transcription" ); const text = result?.text || ""; logTail(`Transcribed OK: ${text.length} chars`); return text; }catch(err){ logTail(`Transcription error: ${err?.message || err}`); return ""; } } // ===== Open-web hunt (free sources) ===== async function fetchText(url) { try{ const r = await fetchWithTimeout(url, {}, HUNT_TIMEOUT_MS); const ct = r.headers.get("content-type") || ""; if (!r.ok || !/text|json|xml|html/i.test(ct)) return ""; const raw = await r.text(); return textClamp(raw.replace(//gi, " ").replace(//gi, " ").replace(/<[^>]+>/g, " ")); }catch{ return ""; } } async function rdapDomain(domain){ const txt = await fetchText(`https://rdap.org/domain/${encodeURIComponent(domain)}`); if (!txt) return null; try{ return JSON.parse(txt); } catch { return null; } } async function dnsMxOk(domain){ try{ const r = await fetchWithTimeout(`https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=MX`, {}, HUNT_TIMEOUT_MS); if (!r.ok) return false; const j = await r.json(); return Array.isArray(j?.Answer) && j.Answer.length > 0; }catch{ return false; } } // Reverse-phone pages + DDG fallback async function huntByPhone(e164){ const nd = onlyDigits(e164).replace(/^1/, ""); const pages = [ `https://www.800notes.com/Phone.aspx/1-${nd}`, `https://whocallsme.com/Phone-Number.aspx/${nd}` ]; let snippets = []; for (const u of pages) { const t = await fetchText(u); if (t) snippets.push(`URL: ${u}\nTEXT: ${t.slice(0, 3000)}`); } if (snippets.length === 0) { const variants = [`+1${nd}`, `(${nd.slice(0,3)}) ${nd.slice(3,6)}-${nd.slice(6)}`, `${nd.slice(0,3)}-${nd.slice(3,6)}-${nd.slice(6)}`]; const q = encodeURIComponent(`"${variants.join('" OR "')}"`); const ddgUrl = `https://duckduckgo.com/html/?q=${q}`; const html = await fetchText(ddgUrl); if (html) { const blocks = html.split(' { const title = (b.match(/>([^<]+)<\/a>/) || [,""])[1]; const cite = (b.match(/result__url[^>]*>([^<]+)/) || [,""])[1]; const desc = (b.match(/result__snippet[^>]*>([^<]+)/) || [,""])[1]; return `DDG[${i+1}] ${title}\nURL: ${cite}\nTEXT: ${desc}`; }).filter(Boolean); if (items.length) snippets = items; } } return snippets.join("\n\n---\n\n"); } async function crawlSite(domain){ const urls = [ `https://${domain}/`, `https://${domain}/about`, `https://${domain}/contact`, `https://${domain}/privacy` ]; const parts = []; for (const u of urls){ const t = await fetchText(u); if (t) parts.push(`URL: ${u}\nTEXT: ${t}`); } return parts.join("\n\n---\n\n"); } // ===== AI extraction (strict JSON) ===== async function aiExtract({ prompt, model = AI_EXTRACT_MODEL }) { if (!AI_EXTRACT_ON) { logTail("AI extract skip: AI_EXTRACT_ON=0"); return null; } if (!OPENAI_API_KEY) { logTail("AI extract skip: OPENAI_API_KEY missing"); return null; } try{ const openai = new OpenAI({ apiKey: OPENAI_API_KEY }); const resp = await withTimeout( openai.chat.completions.create({ model, messages: [ { role: "system", content: "Output strict JSON only. No prose."}, { role: "user", content: prompt.slice(0, 20000) } ], temperature: 0.1, }), 9000, "ai-extract" ); let raw = resp.choices?.[0]?.message?.content || "{}"; raw = raw.replace(/^\s*```(?:json)?\s*|\s*```\s*$/g, ""); try { const j = JSON.parse(raw); logTail(`AI extract OK: fields=${Object.keys(j||{}).length}`); return j; } catch { logTail("AI extract JSON parse failed"); return null; } }catch(e){ logTail(`AI extract error: ${e?.message || e}`); return null; } } // ===== GAS post ===== async function postEvidence(payload){ if (!GOOGLE_INTAKE_EXEC){ logTail("WARN: GOOGLE_INTAKE_EXEC unset; skipping POST"); return { skipped: true }; } const r = await fetchWithTimeout(GOOGLE_INTAKE_EXEC, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "evidence", payload }) }, 7000); const txt = await r.text(); logTail(`GAS POST → ${r.status} ${r.statusText} :: ${txt.slice(0,160)}${txt.length>160?"…":""}`); return { status: r.status, body: txt }; } // ===== Two-Tier Enrichment (core) ===== async function enrichTwoTier({ transcript, fromNum, initialWebsite, initialEmail, initialContact, initialCompany, initialCallback, modeLabel }) { // Tier-1: Direct Extract (trust entities present IN the text) let company_name = initialCompany || ""; let contact_name = initialContact || ""; let email = initialEmail || ""; let website = initialWebsite || ""; let callback_number = initialCallback || ""; // If we still have gaps, ask AI to extract strictly from transcript ONLY (no web) let tier1AI = null; try { const prompt = ` Extract ONLY what is explicitly present in the message TEXT below. Return strict JSON with keys: company_name, contact_name, email, website, callback_number, flags[], notes. No guessing. If absent in the TEXT, leave fields empty or arrays empty. TEXT: ${(transcript||"").slice(0, 8000)} `; tier1AI = await aiExtract({ prompt }); } catch {} if (tier1AI) { if (!company_name && tier1AI.company_name) company_name = String(tier1AI.company_name).trim(); if (!contact_name && tier1AI.contact_name) contact_name = String(tier1AI.contact_name).trim(); if (!email && tier1AI.email) email = String(tier1AI.email).toLowerCase().trim(); if (!website && tier1AI.website) website = normalizeDomain(String(tier1AI.website).trim()); if (!callback_number && tier1AI.callback_number) callback_number = String(tier1AI.callback_number).trim(); } // If tier-1 already found something actionable (any of these), STOP here (no web/corroboration needed). const tier1Hit = !!(company_name || contact_name || website || email || callback_number); if (tier1Hit) { logTail(`${modeLabel} Tier-1 hit: company=${company_name||"-"} contact=${contact_name||"-"} site=${website||"-"} email=${email||"-"} cb=${callback_number||"-"}`); return { company_name, contact_name, email, website, callback_number, tier: 1, webSnippets: "", mx_ok: "" }; } // Tier-2: Fallback enrichment (light web hunt + AI fusion) if (!OPEN_WEB_HUNT_ON) { logTail(`${modeLabel} Tier-2 skipped: OPEN_WEB_HUNT_ON=0`); return { company_name, contact_name, email, website, callback_number, tier: 1, webSnippets: "", mx_ok: "" }; } let webSnippets = ""; let mx_ok = ""; try { // If we have a domain or email domain, crawl site + RDAP + MX const domain = website || getDomainFromEmail(email); if (domain) { const site = await crawlSite(normalizeDomain(domain)); const rdap = await rdapDomain(normalizeDomain(domain)); const mxok = await dnsMxOk(normalizeDomain(domain)); mx_ok = mxok ? "true" : "false"; webSnippets = [ site && `SITE:\n${site}`, rdap && `RDAP:\n${JSON.stringify(rdap).slice(0, 4000)}`, `MX_OK:${mx_ok}` ].filter(Boolean).join("\n\n---\n\n"); } else if (initialCallback || fromNum) { const phone = initialCallback || fromNum || ""; webSnippets = await huntByPhone(phone); } } catch (e) { logTail(`${modeLabel} web-hunt error: ${e?.message || e}`); } // AI fusion with web snippets as optional context let tier2AI = null; try { const prompt = ` Using the message TEXT and optional WEB SNIPPETS, extract verifiable facts. Return strict JSON: company_name, contact_name, email, website, callback_number, flags[], notes, source_confidence (0..1), success_score (0..1), ident_signals_json. TEXT: ${(transcript||"").slice(0, 8000)} WEB SNIPPETS: ${(webSnippets||"").slice(0, 12000)} `; tier2AI = await aiExtract({ prompt }); } catch {} if (tier2AI) { // Apply conservative merge (require some confidence if the value wasn't already in the text) const conf = typeof tier2AI.source_confidence === "number" ? tier2AI.source_confidence : 0; const accept = conf >= 0.60 || hasEntityHints(transcript); // if transcript had hints, we're lenient if (accept) { if (!company_name && tier2AI.company_name) company_name = String(tier2AI.company_name).trim(); if (!contact_name && tier2AI.contact_name) contact_name = String(tier2AI.contact_name).trim(); if (!email && tier2AI.email) email = String(tier2AI.email).toLowerCase().trim(); if (!website && tier2AI.website) website = normalizeDomain(String(tier2AI.website).trim()); if (!callback_number && tier2AI.callback_number) callback_number = String(tier2AI.callback_number).trim(); } else { logTail(`${modeLabel} Tier-2 AI fields dropped due to low confidence`); } } return { company_name, contact_name, email, website, callback_number, tier: 2, webSnippets, mx_ok }; } // ===== Routes ===== app.get("/health", (req,res)=> res.type("text/plain").send("OK")); app.get("/__tail.txt", (req,res)=> res.type("text/plain").send(tail.join("\n"))); app.get("/__routes", (req,res)=>{ res.json({ routes: [ "GET /health", "GET /__routes", "GET /__tail.txt", "GET /webhooks/twilio/voice-preview", "POST /webhooks/twilio/voice", "POST /webhooks/twilio/sms", "POST /recording-status" ], PUBLIC_BASE_URL, GREETING_TTS, GREETING_VOICE, AI_PARSE_ON, AI_WEB_ENRICH_ON, LOOKUP_ON, INJECT_2, INJECT_2_DELAY_MS, AI_EXTRACT_ON, AI_EXTRACT_MODEL, OPEN_WEB_HUNT_ON }); }); app.get("/webhooks/twilio/voice-preview", (req,res)=>{ const twiml = voiceTwiml({ recordingAction: `${PUBLIC_BASE_URL}/recording-status` }); xml(res, twiml); }); // Voice webhook → TwiML (TTS → Record) app.post("/webhooks/twilio/voice", (req,res)=>{ logTail(`POST /webhooks/twilio/voice :: From=${req.body.From||""} To=${req.body.To||""} Sid=${req.body.CallSid||""}`); const twiml = voiceTwiml({ recordingAction: `${PUBLIC_BASE_URL}/recording-status` }); xml(res, twiml); }); // Recording status → transcribe → two-tier enrich → heuristics → post app.post("/recording-status", async (req,res)=>{ try{ const { RecordingUrl, RecordingSid, RecordingDuration, CallSid, From, To, CallStatus } = req.body || {}; logTail(`POST /recording-status :: RecSid=${RecordingSid||""} CallSid=${CallSid||""} Url=${RecordingUrl||""}`); // safe .mp3 handling (Twilio-style or already .mp3) let recording_mp3 = RecordingUrl || ""; if (recording_mp3 && !/\.mp3($|\?)/i.test(recording_mp3)) recording_mp3 += ".mp3"; // 1) Transcribe let transcript = ""; try { transcript = await withTimeout(transcribeIfEnabled(recording_mp3), 14000, "transcribe wrapper"); } catch (e) { logTail(`Transcribe wrapper timeout/fail: ${e?.message || e}`); } const trivial = isTrivialText(transcript); if (trivial) logTail("Voice: trivial text — enrichment minimized"); // Initial regex extract (Tier-1 seeds) let email = extractEmail(transcript); let website = extractWebsite(transcript); let { contact_name, company_name } = extractContactAndCompany(transcript); const foundNumbers = extractCallbackNumbers(transcript); let callback_number = foundNumbers[0] || (From || ""); logTail(`Voice callbacks found: ${JSON.stringify(foundNumbers)}`); // Two-tier enrichment let aiTier = { company_name, contact_name, email, website, callback_number, tier: 0, webSnippets: "", mx_ok: "" }; if (!trivial) { aiTier = await enrichTwoTier({ transcript, fromNum: From || "", initialWebsite: website, initialEmail: email, initialContact: contact_name, initialCompany: company_name, initialCallback: callback_number, modeLabel: "VOICE" }); ({ company_name, contact_name, email, website, callback_number } = aiTier); } // Fill gaps from domain/email if (!company_name && website) company_name = inferCompanyFromDomain(website); if (!website && email) website = normalizeDomain(getDomainFromEmail(email)); // Heuristics + flags let flags = ""; let notes = ""; let source_confidence = ""; let success_score = ""; let ident_signals_json = ""; if (!trivial) { const h = keywordHeuristics(transcript); flags = h.flags.join(", "); success_score = h.success.toFixed(2); } else { flags = ""; success_score = ""; } // RPS + defendant id const flagsArr = flags.split(",").map(s=>s.trim()).filter(Boolean); const rps = computeRps({ flags: flagsArr, transcript, callback_number }).toFixed(2); const defendant_seed = makeDefendantSeed({ website, callback_number, email, company_name, from_number: From || "" }); const defendant_id = makeDefendantIdFromSeed(defendant_seed); const evidence = { timestamp: humanTimestampET(), from_number: From || "", to_number: To || "", direction: "inbound", channel: "voice", call_sid: CallSid || "", recording_sid: RecordingSid || "", recording_url: recording_mp3, duration_sec: Number(RecordingDuration || 0), transcript, flags, defendant_id, defendant_seed, company_name, contact_name, email, callback_number, website, rps, notes, source_confidence, letter_link: "", email_status: "", lob_status: "", success_score, ident_signals_json, serve_priority: "", fallback_ready: "", rmd_provider: "", rmd_email: "", rmd_phone: "", email_mx_valid: aiTier.mx_ok || "", enrich_carrier_contacts: "", enrich_npi_json: "", enrich_sos: "", web_hunt_notes: (aiTier.webSnippets||"").slice(0, 500), test_mode: TEST_MODE, ai_parse_on: AI_PARSE_ON, ai_web_enrich_on: AI_WEB_ENRICH_ON, open_web_hunt_on: OPEN_WEB_HUNT_ON, ai_extract_on: AI_EXTRACT_ON, lookup_on: LOOKUP_ON, public_base_url: PUBLIC_BASE_URL, call_status: CallStatus || "" }; logTail(`POSTing evidence (voice) to GAS: call_sid=${CallSid||""} rec_sid=${RecordingSid||""} rps=${rps} seed=${defendant_seed} tier=${aiTier.tier}`); await postEvidence(evidence); res.status(200).type("text/plain").send("OK"); }catch(err){ logTail(`ERR /recording-status :: ${err?.stack||err}`); res.status(200).type("text/plain").send("OK"); } }); // ===== SMS webhook → two-tier enrich → heuristics → post app.post("/webhooks/twilio/sms", async (req, res) => { try { const { From = "", To = "", Body = "", SmsSid = "", MessageSid = "" } = req.body || {}; logTail(`POST /webhooks/twilio/sms :: From=${From} To=${To} Sid=${MessageSid||SmsSid} BodyLen=${(Body||"").length}`); const transcript = (Body || "").toString(); const trivial = isTrivialText(transcript); if (trivial) logTail("SMS: trivial text — enrichment minimized"); // Initial regex extract (Tier-1 seeds) let email = extractEmail(transcript); let website = extractWebsite(transcript); let { contact_name, company_name } = extractContactAndCompany(transcript); const foundNumbers = extractCallbackNumbers(transcript); let callback_number = foundNumbers[0] || From; logTail(`SMS callbacks found: ${JSON.stringify(foundNumbers)}`); // Two-tier enrichment let aiTier = { company_name, contact_name, email, website, callback_number, tier: 0, webSnippets: "", mx_ok: "" }; if (!trivial) { aiTier = await enrichTwoTier({ transcript, fromNum: From || "", initialWebsite: website, initialEmail: email, initialContact: contact_name, initialCompany: company_name, initialCallback: callback_number, modeLabel: "SMS" }); ({ company_name, contact_name, email, website, callback_number } = aiTier); } // Fill gaps from domain/email if (!company_name && website) company_name = inferCompanyFromDomain(website); if (!website && email) website = normalizeDomain(getDomainFromEmail(email)); // Heuristics + flags let flags = ""; let notes = ""; let source_confidence = ""; let success_score = ""; let ident_signals_json = ""; if (!trivial) { const h = keywordHeuristics(transcript); flags = h.flags.join(", "); success_score = h.success.toFixed(2); } else { flags = ""; success_score = ""; } // RPS + deterministic defendant const flagsArr = flags.split(",").map(s=>s.trim()).filter(Boolean); const rps = computeRps({ flags: flagsArr, transcript, callback_number }).toFixed(2); const defendant_seed = makeDefendantSeed({ website, callback_number, email, company_name, from_number: From || "" }); const defendant_id = makeDefendantIdFromSeed(defendant_seed); const evidence = { timestamp: humanTimestampET(), from_number: From, to_number: To, direction: "inbound", channel: "sms", call_sid: MessageSid || SmsSid || "", recording_sid: "", recording_url: "", duration_sec: 0, transcript, flags, defendant_id, defendant_seed, company_name, contact_name, email, callback_number, website, rps, notes, source_confidence, letter_link: "", email_status: "", lob_status: "", success_score, ident_signals_json, serve_priority: "", fallback_ready: "", rmd_provider: "", rmd_email: "", rmd_phone: "", email_mx_valid: aiTier.mx_ok || "", enrich_carrier_contacts: "", enrich_npi_json: "", enrich_sos: "", web_hunt_notes: (aiTier.webSnippets||"").slice(0, 500), test_mode: TEST_MODE, ai_parse_on: AI_PARSE_ON, ai_web_enrich_on: AI_WEB_ENRICH_ON, open_web_hunt_on: OPEN_WEB_HUNT_ON, ai_extract_on: AI_EXTRACT_ON, lookup_on: LOOKUP_ON, public_base_url: PUBLIC_BASE_URL, call_status: "" }; logTail(`POSTing evidence (sms) to GAS: msg_sid=${MessageSid||SmsSid} rps=${rps} seed=${defendant_seed} tier=${aiTier.tier}`); await postEvidence(evidence); res.status(200).type("text/plain").send("OK"); } catch (err) { logTail(`ERR /webhooks/twilio/sms :: ${err?.stack || err}`); res.status(200).type("text/plain").send("OK"); } }); // ===== Start ===== app.listen(PORT, ()=> { logTail(`CallSlayer v3 up on :${PORT}`); logTail(`Base: ${PUBLIC_BASE_URL}`); logTail(`Greeting TTS (${GREETING_VOICE}): ${GREETING_TTS}`); if (INJECT_2) logTail(`DTMF inject '2' enabled after ~${Math.ceil(INJECT_2_DELAY_MS/1000)}s`); });