// ==UserScript== // @name Ebenezer // @namespace http://tampermonkey.net/ // @version 0.1 // @description Embed Ebenezer in HubSpot // @author StudentRoomStay // @match https://app.hubspot.com/contacts/2113514/contact/* // @match https://app.hubspot.com/contacts/2113514/deal/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // ==/UserScript== const version = "1.0.5"; console.log(`### [ebenezer-run] Version is: ${version}`); const recordContactIndicator = "0-1"; const recordDealIndicator = "0-3"; const recordTicketIndicator = "0-5"; const environments = { local: "http://localhost:3000", beta: "https://test-ledger.studentroomstay.com/web", prod: "https://ledger.studentroomstay.com/web", }; let state, currentHref, ebenezer; window.addEventListener("message", ebenezerReady); waitForPageLoad(); // Code to observe changes to parts of the DOM const observer = new MutationObserver(pageChanged); observer.observe(document, { childList: true, subtree: true }); function waitForPageLoad() { // prevent duplicate loads ebenezer = document.getElementById("ebenezer"); if (!ebenezer) { state = "waiting"; console.log("EBENEZER", state); ebenezer = null; currentHref = document.location.href; } else { state = "initialized"; currentHref = document.location.href; ebenezer.style.width = "60px"; ebenezer.style.minWidth = "60px"; } } function pageChanged(changes, observer) { if (state === "waiting") { setupEbenezer(); } else if (state === "initialized") { loadData(); } else if (currentHref !== document.location.href) { waitForPageLoad(); } } function setupEbenezer() { const insertionPoint = getInsertionPoint(); // prevent duplicate loads ebenezer = document.getElementById("ebenezer"); if (!ebenezer && insertionPoint) { ebenezer = insertEbenezer(insertionPoint); fixFormatting(); } } function loadData() { try { const data = scrapeData(); console.log("LOADED DATA", data); if (data) { sendData(ebenezer, data); state = "loading"; console.log("EBENEZER", state); } } catch (err) { console.log("LOADING ERROR", err); // ignore; the data isn't ready yet } } function ebenezerReady(event) { if (event.data === "ebenezer:init") { state = "initialized"; console.log("EBENEZER", state); loadData(); } else if (event.data === "ebenezer:login") { console.log("EBENEZER LOGIN REQUIRED"); ebenezer.style.width = "auto"; ebenezer.style.minWidth = "240px"; } else if (event.data === "ebenezer:ready") { state = "loaded"; console.log("EBENEZER", state); ebenezer.style.width = "auto"; ebenezer.style.minWidth = "400px"; } else if (event.data === "ebenezer:close") { ebenezer.style.width = "0px"; ebenezer.style.minWidth = "0px"; } } function getInsertionPoint() { return document.querySelector("div[data-unit-test='filterAndMiddle']"); } function fixFormatting() { const timeline = document.querySelector( "div[data-selenium-test='timeline-wrapper']", ); timeline.style.minWidth = "500px"; } function insertEbenezer(insertionPoint) { const ebenezer = document.createElement("iframe"); const env = localStorage.getItem("ebenezer_env"); const page_type = window.location.href.split("/")[5]; ebenezer.src = environments[env] + "/#/hubspot/" + page_type; ebenezer.id = "ebenezer"; ebenezer.style.width = "60px"; ebenezer.style.minWidth = "60px"; ebenezer.style.border = "0"; ebenezer.style.borderLeft = "medium solid green"; insertionPoint.appendChild(ebenezer); return ebenezer; } function sendData(ebenezer, data) { console.log("Ebenezer Data", data); ebenezer.contentWindow.postMessage(data, "*"); } function scrapeData() { const href = window.location.href; const page_type = href.split("/")[5]; if (page_type === "contact") { return scrapeContactPage(); } else if (page_type === "ticket") { return scrapeTicketPage(); } else if (page_type === "record") { const recordIndicator = href.split("/")[6]; if (recordIndicator === recordContactIndicator) { return scrapeContactPage(true); } else if (recordIndicator === recordTicketIndicator) { return scrapeTicketPage(true); } else { return { page_type: "unknown" }; } } return { page_type: "unknown" }; } let moreClicked = false; function scrapeDealPage(isRecordPage = false) { const dealDetails = document.querySelector( "div[data-selenium-test='deal-highlight-details']", ); const associatedNodes = [ ...document.querySelectorAll( "div[data-sidebar-card-type='AssociatedObjectsCard']", ), ]; const contactsList = associatedNodes.find((h5) => h5.innerText.includes("Contacts"), ); const moreContacts = contactsList.querySelector( "button[data-selenium-test='associated-objects-card-view-more']", ); if (moreContacts) { if (!moreClicked) { // only click this once; hubspot gets funny if you click it more than once moreContacts.click(); moreClicked = true; } // wait until the button is gone return null; } if (!dealDetails || !contactsList) { return null; } return { page_type: "deal", deal: scrapeDeal(dealDetails, isRecordPage), contacts: scrapeAssociatedContacts(contactsList, isRecordPage), }; } function scrapeTicketPage(isRecordPage = false) { const ticketDetails = document.querySelector( "div[data-selenium-test='ticket-highlight-details']", ); const contactsList = document.querySelector('[data-sidebar-key="Contacts"]'); const dealsList = document.querySelector('[data-sidebar-key="Deals"]'); const moreContacts = contactsList.querySelector( "button[data-selenium-test='associated-objects-card-view-more']", ); if (moreContacts) { if (!moreClicked) { // only click this once; hubspot gets funny if you click it more than once moreContacts.click(); moreClicked = true; } // wait until the button is gone return null; } if (!ticketDetails || !contactsList || !dealsList) { return null; } return { page_type: "ticket", ticket: scrapeTicket(ticketDetails, isRecordPage), contacts: scrapeAssociatedContacts(contactsList, isRecordPage), deals: scrapeAssociatedDeals(dealsList, isRecordPage), }; } function scrapeContactPage(isRecordPage = false) { const contactDetailsHTML = document.querySelector( "div[data-selenium-test='contact-highlight-details']", ); const associatedNodes = [ ...document.querySelectorAll( "div[data-sidebar-card-type='AssociatedObjectsCard']", ), ]; const dealsListHTML = associatedNodes.find( (div) => typeof div.getAttribute("data-sidebar-key") === "string" && div.getAttribute("data-sidebar-key").includes("Deals"), ); if (!contactDetailsHTML || !dealsListHTML) { return null; } return { page_type: "contact", contact: scrapeContact(contactDetailsHTML, isRecordPage), deals: scrapeAssociatedDeals(dealsListHTML, isRecordPage), }; } function scrapeDeal(dealHTML, isRecordPage) { const hubspot_url = window.location.href; return { hubspot_url: hubspot_url, hubspot_id: hubspot_url.split("/")[isRecordPage ? 7 : 6], name: dealHTML.querySelector("span[data-selenium-test='highlightTitle']") .innerText, }; } function scrapeTicket(ticketHTML, isRecordPage) { const hubspot_url = window.location.href; const pipeline = ticketHTML.querySelector( "button[data-selenium-test='pipeline-select'] span", )?.innerText; if (!pipeline || /^[0-9]*$/.test(pipeline)) { throw new Error("pipeline not yet loaded"); } return { hubspot_url: hubspot_url, hubspot_id: hubspot_url.split("/")[isRecordPage ? 7 : 6], name: ticketHTML.querySelector("span[data-selenium-test='highlightTitle']") .innerText, pipeline, }; } function scrapeContact(contactHTML, isRecordPage) { const hubspot_url = window.location.href; return { hubspot_url: hubspot_url, hubspot_id: hubspot_url.split("/")[isRecordPage ? 7 : 6], name: contactHTML.querySelector("span[data-selenium-test='highlightTitle']") .firstChild.innerText, }; } function scrapeAssociatedDeals(dealList, isRecordPage) { const dealNodes = [ ...dealList.querySelectorAll("div[data-selenium-test='chicklet']"), ]; deals = []; dealNodes.forEach((d) => { const a = d.querySelector( "div[data-selenium-test='deal-chicklet-title'] > a", )?.href; const dealName = d?.querySelector( "span[data-selenium-test='property-input-dealname']", ); deals.push({ name: dealName?.innerText, hubspot_url: a, hubspot_id: a?.split("/")[isRecordPage ? 7 : 6], }); }); console.log("### [ebenezer-run] Deals: ", deals); return deals; } function scrapeAssociatedContacts(contactsList, isRecordPage) { const contactNodes = [ ...contactsList.querySelectorAll("div[data-selenium-test='chicklet']"), ]; contacts = []; contactNodes.forEach((c) => { const hubspot_url = c.querySelector( "span[data-selenium-test='contact-chicklet-title-link']", ).parentNode.href; contacts.push({ name: c.querySelector( "span[data-selenium-test='contact-chicklet-title-link']", ).innerText, customer_type: c.querySelector( "span[data-selenium-test='formatted-property-customer_type']", ).innerText, hubspot_url: hubspot_url, hubspot_id: hubspot_url.split("/")[isRecordPage ? 7 : 6], }); }); return contacts; }