281 lines
8.5 KiB
JavaScript
281 lines
8.5 KiB
JavaScript
// 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()})();`,
|
|
};
|