// Main-world probe injected into every account document (top page and CCP // iframes). Serialized with toString(), so this function must be fully // self-contained — no requires, no closed-over values. // // Jobs: detect the signed-in agent, detect contacts arriving/leaving, run // Available/Offline commands, and force spellcheck on chat composers. function mainWorldProbe() { if (window.__cdProbe) return; window.__cdProbe = true; const post = function (m) { const payload = Object.assign({ __cdmsg: true }, m); window.postMessage(payload, "*"); try { if (window.top && window.top !== window) window.top.postMessage(payload, "*"); } catch (e) {} }; // ---------- signed-in agent: name + auth ---------- let namePosted = ""; // Login-form furniture is not a name — "Remember username" once got saved as // an agent name because the username field's class matched the selector. const NOT_A_NAME = /remember|username|password|sign\s?in|log\s?in|forgot|welcome/i; const postName = function (n) { n = (n || "").trim(); if (!n || NOT_A_NAME.test(n)) return; if (n !== namePosted) { namePosted = n; post({ type: "agent", name: n }); } }; const looksLikeLogin = function () { const t = (document.body && document.body.innerText) || ""; return /Please log in|Forgot Password/i.test(t) && t.length < 900; }; function scrapeName() { try { const text = (document.body && document.body.innerText) || ""; if (looksLikeLogin()) return; const patterns = [ /Welcome\s+([^\s\n][^\n]{0,60})/, /Signed in as\s+([^\s\n][^\n]{0,60})/i, /Logged in as\s+([^\s\n][^\n]{0,60})/i, ]; for (let i = 0; i < patterns.length; i++) { const m = text.match(patterns[i]); if (m) return postName(m[1]); } const el = document.querySelector( '[data-testid*="user" i],[class*="userName" i],[class*="agentName" i]' ); if (el && el.textContent) postName(el.textContent); } catch (e) {} } // ---------- agent status control (bulk Available/Offline) ---------- function doSetStatus(kind) { const c = window.connect; const fail = function (error) { post({ type: "status-result", kind: kind, ok: false, error: error }); }; if (!c || !c.Agent) { // Connect lives in the CCP iframe. Frames without Streams stay quiet so // a top-frame "not signed in" doesn't beat a successful iframe reply. // Login pages (top, no iframes, no connect) still fail fast. try { if (window !== window.top) return; if (document.querySelectorAll("iframe").length) return; } catch (e) { return; } return fail("not signed in"); } let agent; try { agent = new c.Agent(); } catch (e) { return fail("not signed in"); } let target; try { const wanted = kind === "offline" ? c.AgentStateType.OFFLINE : c.AgentStateType.ROUTABLE; target = (agent.getAgentStates() || []).find(function (s) { return s.type === wanted; }); } catch (e) { return fail("states unavailable"); } if (!target) return fail("no matching state"); try { agent.setState( target, { success: function () { post({ type: "status-result", kind: kind, ok: true, state: target.name }); }, failure: function () { fail("rejected by Connect"); }, }, { enqueueNextState: true } ); } catch (e) { fail((e && e.message) || "error"); } } window.addEventListener("message", function (e) { const d = e.data; if (!d || d.__cdcmd !== true) return; if (d.cmd === "set-status") doSetStatus(d.kind); }); // ---------- Streams hookup ---------- let streams = false; const seenContacts = {}; function handleContact(contact) { let id; try { id = contact.getContactId(); } catch (e) { return; } if (!id || seenContacts[id]) return; seenContacts[id] = true; let q = ""; try { q = (contact.getQueue() && contact.getQueue().name) || ""; } catch (e) {} const gone = function () { delete seenContacts[id]; post({ type: "gone", contactId: id }); }; post({ type: "incoming", contactId: id, queue: q }); ["onConnected", "onEnded", "onMissed", "onDestroy", "onAccepted"].forEach(function (ev) { try { contact[ev] && contact[ev](gone); } catch (e) {} }); } function tryStreams() { const c = window.connect; if (!c || typeof c.contact !== "function" || typeof c.agent !== "function") return false; try { c.contact(function (contact) { handleContact(contact); }); } catch (e) { return false; } streams = true; try { c.agent(function (agent) { post({ type: "auth", signedIn: true }); try { postName(agent.getName && agent.getName()); } catch (e) {} let lastState = ""; const sendState = function () { try { const s = agent.getState(); const key = ((s && s.name) || "") + "|" + ((s && s.type) || ""); if (key === lastState) return; lastState = key; post({ type: "state", stateName: (s && s.name) || "", stateType: (s && s.type) || "" }); } catch (e) {} }; sendState(); try { agent.onStateChange(sendState); } catch (e) {} try { agent.onRefresh(sendState); } catch (e) {} // Catch a chat that was already ringing before we hooked contact(). try { const contacts = agent.getContacts && agent.getContacts(); if (contacts && contacts.length) { for (let i = 0; i < contacts.length; i++) handleContact(contacts[i]); } } catch (e) {} }); } catch (e) {} post({ type: "detection", mode: "streams" }); return true; } setInterval(function () { if (!streams) tryStreams(); }, 1000); tryStreams(); // Name + login-page detection loop. Keeps going until the name lands, because // sign-in can happen long after the page first loads. let loginPosted = false; const nameTimer = setInterval(function () { if (!streams) { if (looksLikeLogin()) { if (!loginPosted) { loginPosted = true; post({ type: "auth", signedIn: false }); } } else { loginPosted = false; } } scrapeName(); if (namePosted && streams) clearInterval(nameTimer); }, 2500); scrapeName(); // DOM incoming-chat watch — always on, even if Streams hooked. // Agent Workspace shows "Incoming chat" / "Accept chat" in the page even // when connect.contact() never fires. Skipping this once streams=true is // why the sidebar stayed on "No waiting chats" while CCP was ringing. const INCOMING_RE = /incoming chat|incoming contact|accept chat|reject chat|customer is waiting|new chat|chat request/i; let domSeen = false; const scanIncoming = function () { const hit = INCOMING_RE.test((document.body && document.body.innerText) || ""); if (hit && !domSeen) { domSeen = true; post({ type: "incoming", contactId: "dom-incoming", queue: "" }); } else if (!hit && domSeen) { domSeen = false; post({ type: "gone", contactId: "dom-incoming" }); } }; try { new MutationObserver(scanIncoming).observe(document.documentElement, { childList: true, subtree: true, characterData: true, }); } catch (e) {} setInterval(scanIncoming, 1500); scanIncoming(); // Extra Streams-only fallback if the SDK never appears at all (login page). setTimeout(function () { if (streams) return; post({ type: "detection", mode: "dom" }); }, 20000); // Connect's composer often sets spellcheck="false". Force it back on so // Chromium's checker and our context-menu suggestions can run. document.addEventListener( "focusin", function (e) { const t = e.target; if (!t) return; const tag = (t.tagName || "").toUpperCase(); const type = (t.type || "").toLowerCase(); if (type === "password" || type === "hidden") return; if (t.isContentEditable || tag === "TEXTAREA" || tag === "INPUT") { try { t.spellcheck = true; t.setAttribute("spellcheck", "true"); } catch (err) {} } }, true ); } module.exports = { mainWorldProbe, probeScript: `(${mainWorldProbe.toString()})();`, };