ninco / Muffin for Callyouback

// ==UserScript==
// @name         Muffin for Callyouback
// @namespace    ninco
// @version      1.2
// @description  Add profile reference link on muffin.net
// @author       you
// @copyright    2020, ninco (https://openuserjs.org/users/ninco)
// @license      MIT
// @downloadURL  https://openuserjs.org/install/ninco/Muffin_for_Callyouback.user.js
// @updateURL    https://openuserjs.org/meta/ninco/Muffin_for_Callyouback.meta.js
// @supportURL   https://openuserjs.org/scripts/ninco/Muffin_for_Callyouback/issue/new

// @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.4.1/jquery.min.js
// @require      https://raw.githubusercontent.com/bitlyfied/js-image-similarity/master/simi.js
// @match        naberu.com/*
// @grant        GM_xmlhttpRequest
// @connect      keksik.net
// @run-at       document-idle
// ==/UserScript==

/* globals $ */


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

let DEBUG = false;
let muffinCorsFetchFlag = true;
let corsInterval = 2700; // delay between CORS requests in ms (to avoid captcha)

let limitCards = 5*DEBUG; // total 50 on page
if (!DEBUG) { console.log = function () {}; }


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

let pathname = window.location.pathname;

//  card profile (URL ends with digits)
if (/^\d+$/.test(pathname.split("/").slice(-1)[0])) {
  console.log("Parsing number in profle");
  let phoneNumber = $(".mob-hidden.pc-hidden").text().trim();
  $(".phone-item-large").prepend(createMuffinAnchorTag(phoneNumber));
  setTimeout(() => {$("#show_number").click()}, 1000);
}

// search result list (OUTDATED)
else if (false && pathname === "/" || pathname.split("/")[1] === "search") {
  console.log("Parsing search result for profiles");
  let lastCorsTimestamp = 0;
  let cardHeaders = $(".card-block .naberu-item-title");
  if (limitCards) { cardHeaders.splice(limitCards); }
  console.log("Cards URLs parsed: %s", cardHeaders.length);

  cardHeaders.each((i, headerEl) => {
    let profileQuickViewUrl = $(headerEl).find(".link-default").data("href");

    // request each card
    $.ajax({
      url: profileQuickViewUrl,
      cache: false,
      timeout: 2000,
      dataType: "html",
      _header: $(headerEl),
      error: ajaxErrorHandler(),

      success: function (htmlData, status, xhr) {
        let phoneNumber = $(htmlData).find("a.copy-phone").text().slice(-10);
        if (!isPhoneNumberValid(phoneNumber)) {
          console.error("Invalid %s: %s %s", i+1, phoneNumber, this.url);
          return false;
        }
        this._header.append(createStatusLabelTag(phoneNumber), " ", createMuffinAnchorTag(phoneNumber));
        console.log("%s: %s %s", i+1, phoneNumber, this.url);

        // muffin CORS
        if (muffinCorsFetchFlag) {
          setTimeout(() => {
            // console.log("%s CORS for %s", new Date(), phoneNumber);
            muffinCors(phoneNumber, {header: this._header, requestTs: nowStr()});
          }, lastCorsTimestamp+=corsInterval);
        }
      }

    });
  });
}
else {
  console.log("Unhandled 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 isPhoneNumberValid(numberStr) { return /^0\d{9}$/.test(numberStr); } // 0xx44442211
function nowStr() { let now = new Date(); return `${(now.getMinutes()<10?'0':'') + now.getMinutes()}:${(now.getSeconds()<10?'0':'') + now.getSeconds()}`; }


function muffinCors(phone, context) {
    GM_xmlhttpRequest({ // ajax CORS
    method: "GET",
    url: getMuffinUrl(phone),
    context: context,
    onload: function(response) {
      let msg = null;
      console.log("%s muffin response: %s %s [%s]", this.context.requestTs, response.status, response.statusText, response.finalUrl);
      if (response.finalUrl.includes("recaptcha")) { msg = "🤖"; }
      else if (response.status === 200) {
        msg = "✓";
        let images = $(response.responseText).find(".container-items img");
        let image_urls = images.map((_, e) => e.src).toArray();
        let count = Counter(image_urls);
        let biggest_count = count[Object.keys(count)[0]];
        if (biggest_count > 5) msg += " same"; else msg += " diff";
        //simi.hash(image)
      }
      else { msg = "✗"; }
      response.context.header.append(msg);
    },
    onerror: function(response) { console.error("Muffin CORS error %s %s: %s", response.status, response.statusText, this.url); }
  });
};


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));
  };
};


// download and render image in dom
function getImage(url){
  return new Promise(function(resolve, reject){
    var img = new Image()
    img.onload = function(){ resolve(url) }
    img.onerror = function(){ reject(url) }
    img.src = url
  })
};


// count and sort the same elements in array => {num: value}
function Counter(array) {
  var count = {};
  array.forEach(val => { count[val] = (count[val] || 0) + 1; });
  return sortObject(count);
};


// sort object {num: value}
function sortObject(obj) {
  var sortable = [];
  for (var i in obj) sortable.push([i, obj[i]]); // convert {num: value} => [num, value]
  return sortable.sort((a, b) => b[1] - a[1]).reduce((acc, cv) => {acc[cv[0]] = cv[1]; return acc;}, {}); // sort and convert back to {num: value}
};