ninco / Muffin for Ukrgo

// ==UserScript==
// @name         Muffin for Ukrgo
// @namespace    nincognito
// @version      1.0.0
// @description  muffin.net profile reference
// @author       you
// @copyright    2020, nincognito (https://openuserjs.org/users/nincognito)
// @license      NASA-1.3

// @require      https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.18.2/babel.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.16.0/polyfill.js
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @match        kiev.ukrgo.com/*
// @grant        GM_xmlhttpRequest
// @connect      keksik.net
// ==/UserScript==

/* globals $ */


////// CONFIG /////

let requestIntervalMs = 900; // for less then 800ms server blocks with 503
let muffinCorsFetch = false;

let DEBUG = false;
let limitProfiles = 0*DEBUG; // total 56 on single page, 0 - no limit

if (DEBUG === undefined || !DEBUG) console.log = function(){};

////// MAIN //////

if (/\/post_\d+/.test(window.location.pathname)) { // Profile URL
  let click_wait = 1000; // less than 750ms may cause fallstart parsing
  console.log("click \"Показать\" and wait %s sec", click_wait/1000);
  $("#post-phones-show-div > input").click(); // click then wait for chnages to take effect

  // ugly workaround with delayed processing, todo MutationObserver https://stackoverflow.com/a/16726669/1294414
  setTimeout(function(){
    let phoneNumberEl = $("#post-phones-show-div > .post-contacts > span")
    let phoneNumber = phoneNumberEl.text().trim();
    phoneNumberEl.after(" ", createMuffinAnchorTag(phoneNumber));
    console.log("Phone number: %s", phoneNumber);
  }, click_wait);
}

else if (window.location.pathname === "/view_subsection.php" && window.location.search.includes("id_subsection=146")) { // Boardlist URL subsection_id=146
  console.log("Subsection page: %s", window.location.search);
  let headers = $("div.post, .main-content h3"); // banners 3+3, main content 50
  let numberOfHeaders = headers.length;

  // ajax requests in intervals
  let iid = setInterval(() => {
    if (headers.length - limitProfiles) {
      let header = $(headers.splice(0, 1)[0]);
      let profileUrl = header.find(".link_post").get(0).href;
      let count = numberOfHeaders - headers.length;
      console.log("Fetch profile #%s: %s", count, profileUrl);

      // fetch profile page
      $.ajax({
        url: profileUrl,
        method: "GET",
        timeout: 5000,
        dataType: "html",
        _header: header,
        _count: count,
        error: ajaxErrorHandler(),

        success: function(htmlData, status, xhr) {
          let postId, postHash;
          let onclickHandler = $(htmlData).find(".post-contacts input").prop("onclick");
          if (!onclickHandler) {
            console.error("#%s no click handler parsed", this._count);
            this._header.append("📵");
            return true;
          }
          let parsedHandlerArgs = String(onclickHandler).match(/showPhonesWithDigits\(['"](\d+)['"].*['"](\w+)['"]/i);
          [postId, postHash] = parsedHandlerArgs.slice(1, 3);
          console.log("#%s parsed id %s, hash %s", this._count, postId, postHash);

          // fetch hidden phone number
          $.ajax({
            method: "POST",
            url: "/moduls/showphonesnumbers.php",
            data: `i=${postId}&s=${postHash}`,
            dataType: "html",
            _header: this._header,
            _count: this._count,
            error: ajaxErrorHandler(),

            success: function (htmlData, textStatus, jqXHR) {
              if (htmlData) {
                let phoneNumber = validatePhoneNumber($(htmlData).find("span").text());
                console.log("#%s '%s' fetched number: %s", this._count, this.url, phoneNumber);
                this._header.append(createMuffinAnchorTag(phoneNumber));
                if (muffinCorsFetch) { muffinCors(phoneNumber, {header: this._header}); }
              } else {
                console.error("#%s: no data returend from POST %s, data=%s", this._count, this.url, this.data);
              }
            }
          });
        }
      });
    } else { // empty anchors array
      clearInterval(iid);
    }
  }, requestIntervalMs);

}
else {
  console.log("Not 'Uslugi' URL: %s", window.location.href);
}



//////////////// UTILITIES  //////////////////////

function getMuffinUrl(phone) { return "https://keksik.net/phone/" + phone; }
function createMuffinAnchorTag(phone) { return $("<a>").attr({ href: getMuffinUrl(phone), target: "_blank" }).text("🧁"); }
function createStatusLabelTag(msg) { return $("<sub>").css({ "font-weight": 100 }).text(msg); }

function ajaxErrorHandler() {
  return function(xhr, status, exception) {
    console.error("Request failed with '%s' (%s): %s %s %s", status, exception, this.method, this.url, this.data ? `, data ${this.data}` : "");
    let errorIcon = null;
    switch (status) {
      case "error": errorIcon = "🛑"; break;
      case "timeout": errorIcon = "⏳"; break;
      default: errorIcon = "❌";
    };
    this._header.append(errorIcon);
    if (DEBUG) this._header.append("&nbsp;", createStatusLabelTag(exception));
  };
}

function muffinCors(phone, context) {
    GM_xmlhttpRequest({ // ajax CORS
    method: "GET",
    url: getMuffinUrl(phone),
    context: context,
    onload: function(response) {
      console.log("Muffin response: %s %s", response.status, response.statusText);
      response.context.header.append(response.status == 200 ? "✓": "✗");
      if (DEBUG) { response.context.header.append(createStatusLabelTag(response.status)); }
    },
    onerror: function(response) { console.error("Muffin CORS error %s %s: %s", response.status, response.statusText, this.url); }
  });
};

function isPhoneNumberValid(numberStr) { return /^0\d{9}$/.test(numberStr); } // 0xx44442211

function validatePhoneNumber(rawNumber){
  if (isPhoneNumberValid(rawNumber)) {
    return rawNumber; }
  let phoneNumber = null;

  // missed zero, e.g. 671234455
  if (rawNumber.length === 9) {
    phoneNumber = "0" + rawNumber;
  }
  // leading zeros, e.g. 000991234455
  else if (rawNumber.length > 10 && rawNumber.slice(0, rawNumber.length-10).split("").every(e => e === "0")) {
    phoneNumber = rawNumber.slice(rawNumber.length - 10);
  }
  // leading 38, e.g. 380991234455
  else if (rawNumber.length === 12 && rawNumber.slice(0, 2) === "38") {
    phoneNumber = rawNumber.slice(2);
  }
  // multiple numbers, e.g. 0981112233; 0674455666
  else if (rawNumber.includes(";")) {
    phoneNumber = rawNumber.split(";")[0];
  } else {
    console.error("invalid number %s, returning as-is", rawNumber);
    return rawNumber;
  }
  console.log("--- validator corrected from %s to %s", rawNumber, phoneNumber);
  return phoneNumber;
};