Skip to content

Instantly share code, notes, and snippets.

@malaquiasdev
Created September 12, 2024 17:54
Show Gist options
  • Save malaquiasdev/1f03a0963f2bf2fdda426852f5a3718c to your computer and use it in GitHub Desktop.
Save malaquiasdev/1f03a0963f2bf2fdda426852f5a3718c to your computer and use it in GitHub Desktop.

Revisions

  1. malaquiasdev created this gist Sep 12, 2024.
    345 changes: 345 additions & 0 deletions ebenezer-run.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,345 @@
    // ==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;
    }