sjehuda / Black Belt

// ==UserScript== 
// @name        Black Belt 
// @author      Schimon Jehudah, Adv.
// @namespace   org.openuserjs.blackbelt
// @supportURL  https://openuserjs.org/scripts/sjehuda/Black_Belt/issues
// @updateURL   https://openuserjs.org/meta/sjehuda/Black_Belt.meta.js
// @copyright   2022, Schimon Jehudah (http://schimon.i2p)
// @license     MIT; https://opensource.org/licenses/MIT
// @description Finds useful links and displays a top bar with various of links from contact details to media documents, including Metalinks, Podcasts, Syndication Feeds (Atom, JSON & RSS), Torrents and Userscripts.  Also supports Chat, Email, Geoposition, IPFS, Magnet links of eXact Topic (xt), VoIP, Wallet schemes and more.
// @include     *
// @version     1.0.4 
// @run-at      document-end
// @noframes
// @icon        data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48dGV4dCB5PSIuOWVtIiBmb250LXNpemU9IjkwIj7wn6WLPC90ZXh0Pjwvc3ZnPg==
// ==/UserScript==

// NOTE
// Robe icons (Sauna pack) created by Freepik
// https://www.flaticon.com/free-icon/robe_2520932
// https://www.flaticon.com/authors/freepik
// https://www.freepik.com/

// TODO
//
// 0) Tooltip
//    https://www.w3schools.com/howto/howto_css_tooltip.asp
//
// 1) Brand: Access Bar, Alt Bar, Black Bar, Black Robe, Distribar, Distributed Bar, Distribution Bar, Easy Access Bar, Free Bar, Freenet Bar, Handler Bar, Harvest Bar, IETF Bar, IETF Black Bar, IETF ToolBar, Instant Media Bar, Media Bar, Power Bar, Power Download Bar, Reaping Bar, Simple Access Bar, Simple Bar, Super Bar
//
// 2) Recognize btih of 32 and convert it to 40
//
// 3) Check cache links for none 200 code
//    https://bookshelf.theanarchistlibrary.org/library/librarian-previous-announcements-en
//
// 4) FIXME feedx
//    http://freebase.be/db/software.rss.xml
//
// 5) Case insensitive (XPath)
//    String.prototype.toLowerCase()
//
// 6) Fetch button (guess on demand) instead of auto-guess
//    Find file by Hash or ID (Hint: find duplicate chars/strings)
//
// 7) Display software for IPFS, GPS, Monero, RSS, SIP, Tribler, XMPP
//
// 8) Market diaspora*, Linux, Mastodon, ownCloud, RetroShare
//

const types = [ 
  'alt,πŸ“°,feed_a,Follow,Subscribe to News Feed', //ο₯ͺ //ο₯«
  'ext,πŸ“±,app_android,App (Android)', //App Installer (Android)
  'ext,πŸ—οΈ,asc,ASC Key,Additional Sense Code',
  'ext,πŸ”΅οΈ,chromium,Ext. (Chromium)',
  'ext,🐧️,debian,App (Debian)',
  'ext,πŸ“°,feed_x,Follow,Subscribe to News Feed',
  'ext,🐧️,flatpak,App (Linux)',
  'ext,πŸ“οΈ,geo_x,Location',
  'ext,🐯️,mac,App (OSX)',
  'ext,♾️,metalink,Metalink', //∞
  'ext,πŸ“¦οΈ,torrent,Torrent',
  'ext,🐡️,userjs,Userscript', //πŸ’
  'ext,πŸ“₯️,reactos,App (React OS)',
  'ext,🐺️,xpi,Ext. (LibreWolf)', //🦊️
  'uri,🫐️,adc,DC', //βš›οΈ
  'uri,πŸ›οΈ,appstream,App (AppStream)', //πŸ‘œοΈ
  // if (type[5]) { ele.style.transform = 'rotate(90deg)' }
  'uri,πŸ”½,cabal,Cabal', //οΈΎ //πŸ”½ //⧩ //➀
  'uri,πŸ‘οΈβ€πŸ—¨οΈοΈ,chateye,Chat,WARNING: This chat service logs your conversations to its records.  Use Jabber/XMPP',
  'uri,β™ˆ,ed2k,eDonkey',
  'uri,βœ‰οΈ,email,Email,Send an Email Message',
  'uri,πŸ“°,feed,Follow,Subscribe to News Feed',
  'uri,πŸ“οΈ,geo,Location',
  'uri,πŸ•ΈοΈ,ipfs,IPFS', //πŸ—ƒοΈ //βš›οΈ //πŸ’ŽοΈ
  'uri,πŸ—¨οΈ,irc,IRC',
  'uri,πŸŽ™οΈ,itpc,Podcast',
  // (Supports VoIP and OMEMO & OpenPGP encryption methods)
  'uri,πŸ’‘οΈ,jid,Jabber,Chat securely over the XMPP network.',
  //'uri,🧲,magnet,Magnet',
  'uri,ο€’,matrix,Matrix,We recommend using Jabber/XMPP for better privacy.', //#️ //#️⃣️
  'uri,☎️,telephone,Call',
  'uri,πŸ“Ά,udp,Tracker',
  'uri,πŸ“žοΈ,voip,VoIP',
  'uri,πŸͺ™οΈ,wallet,Wallet',
  'url,γŠ™οΈ,i2p,i2p', //㊣
  'url,πŸ§…οΈ,onion,Onion',
  'urn,β™ˆ,aich,eDonkey',
  'urn,πŸͺ©,bitprint,Gnutella2',
  'urn,🌊️,btih,BitTorrent',
  'urn,⛲️,btmh,BitTorrent2', //πŸ’§οΈ
  'urn,β™ˆ,ed2k_u,eDonkey',
  'urn,⏭️,kzhash,Fasttrack',
  'urn,❀️‍πŸ”₯️,md5,Shareaza',
  'urn,❄️,sha1,Frostwire',
  'urn,🌬️,tiger,DC',
  'web,πŸ“±,app_android_w,Store (Android)',
  'web,🍎️,app_ios_w,Store (Apple)',
  'web,πŸ‘οΈβ€πŸ—¨οΈοΈ,chateye_w,Chat,WARNING: This chat service logs your conversations to its records.  Use Jabber/XMPP',
  'web,πŸ—¨οΈ,chat_w,Chat',
  'web,πŸ”΅οΈ,chromium_w,Store (Chrome)',
  'web,πŸ¦…οΈ,falkon,Store (Falkon)',
  'web,🧊,flathub,Store (Flatpak)',
  'web,🐲️,kde,Store (KDE)',
  'web,πŸͺΆοΈ,snapcraft,Store (Snap)',
  'web,πŸͺŸοΈ,windows,Store (Windows)',
  'web,🦎️,xpi_w,Store (Mozilla)'];
//  'pge,βœ‰οΈ,contact,Contact'

// ADC aka DC (Advanced Direct Connect)
const adc = [
  'adc:',
  'adcs:',
  'dchub:'];

const aich = [
  'aich'];

const asc = [
  '.asc.txt',
  '.asc',
  '.gpg',
  '.pgp'];

const app_android = [
  '.apk'];

const app_android_w = [
  '://f-droid.org/app/',
  '://f-droid.org/packages/',
  '://f-droid.org/en/packages/',
  '://play.google.com/store/apps/details?id='];

const app_ios_w = [
  '://apps.apple.com/app/',
  '://apps.apple.com/us/app/'];

const appstream = [
  'appstream:'];

const bitprint = [
  'bitprint'];

const btih = [
  'btih'];

const btmh = [
  'btmh'];

const cabal = [
  'cabal:'];

const chateye = [
  'viber:',
  'tencent:',
  'tg:',
  'whatsapp:'];

const chateye_w = [
  '://t.me',
  '://telegram.me',
  '://chat.whatsapp.com',
  '://wa.me',
  '://api.whatsapp.com/send?phone=',
  '://web.whatsapp.com/send?phone=',
  '://m.me'];

const chat_w = [
  '://webchat.disroot.org/#converse/room?jid=',
  '://yaxim.org/chat/#converse/room?jid=',
  '://anonymous.cheogram.com',
  '://xmpp.org/chat#converse/room?jid=',
  '://yax.im/i/'];

const chromium = [
  '.chrome.zip',
  '.chromium.zip',
  '.crx'];

const chromium_w = [
  '://chrome.google.com/webstore/detail/'];

const debian = [
  '.deb'];

const ed2k = [
  'ed2k:'];

const ed2k_u = [
  'ed2k',
  'ed2khash'];

const email = [
  'mailto:'];

const falkon = [
  '://store.falkon.org/p/'];

const feed = [
  'feed:',
  'news:'];

const feed_a = [
  'atom',
  'rss',
  'stream',
  'rdf'];

const feed_x = [
  '.atom', '.atom.php', '.atom.xml',
  '.rss', '.rss.php', '.rss.xml',
  '.rdf', '.rdf.php', '.rdf.xml'];

const flathub = [
  '://flathub.org/apps/details/'];

const flatpak = [
  '.flatpakref'];

const geo = [
  'geo:',
  'waze:'];

const geo_x = [
  '.gpx',
  '.geojson',
  '.kml',
  '.kmx'];

const i2p = [
  '.i2p',
  '.i2p:'];

const ipfs = [
  'ipfs:',
  'ipns:',
  'dweb:'];

const irc = [
  'ircs:',
  'irc:'];

const itpc = [
  'itpc:'];

// TODO handle ?join and ?message
const jid = [
  'xmpp:'];

const kde = [
  '://store.kde.org/p/'];

const kzhash = [
  'kzhash'];

const mac = [
  '.dmg'];

const matrix = [
  'matrix:'];

const md5 = [
  'md5'];

const metalink = [
  '.meta4',
  '.metalink'];

const windows = [
  '://apps.microsoft.com/store/detail/',
  '://microsoftedge.microsoft.com/addons/detail/',
  '://www.microsoft.com/store/apps/'];

const onion = [
  '.onion',
  '.onion:'];

const sha1 = [
  'sha1'];

// TODO ask snapcraft for path /app/
const snapcraft = [
  '://snapcraft.io/'];

const telephone = [
  'tel:'];

const tiger = [
  'tree:tiger'];

const torrent = [
  '.torrent'];

const udp = [
  'udp:'];

const userjs = [
  '.user.js'];

const voip = [
  'sip:',
  'weixin:',
  'skype:'];

const wallet = [
  'monero:',
  'ethereum:',
  'litecoin:',
  'bitcoin:'];

const reactos = [
  '.exe'];

const xpi = [
  '.xpi',
  '.firefox.zip'];

const xpi_w = [
  '://addons.mozilla.org/firefox/addon/',
  '://addons.mozilla.org/en-US/firefox/addon/'];

let eles = []

// FIXME type to be applied everywhere
let type = [];

function determineType(type) {
  type = type.split(',');
  // TODO find an alternative way
  // THIS IS NOT GOOD!
  array = eval(type[2])
  switch (type[0]) {

    case 'alt':
      extractRel(array, type);
      break;

    case 'ext':
      extractFile(array, type);
      break;

    case 'uri':
      extractURI(array, type);
      break;

    case 'url':
      extractURL(array, type);
      break;

    case 'urn':
      extractURN(array, type);
      break;

    case 'web':
      extractWeb(array, type);
      break;
  }
}

// NOTE TODO semi-recursive callback
// NOTE TODO typeof
function extractFile(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    // FIXME Mainstream to support ends-with
    // fn:ends-with appears to be missing in some engines
    query = [
      '//a[contains(@href, "' + array[i] + '")]/@href',
      '//a[contains(@download, "' + array[i] + '")]/@download'];
//      '//a[ends-with(@href, "' + array[i] + '")]/@href'
//      '//a[ends-with(text(), "' + array[i] + '")]/@href'
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) {
  protocol = location.protocol
  hostname = location.hostname
  console.log(result)
    switch (true) {

      case (result.startsWith('/')):
      result = protocol + '//' + hostname + result;
      break;

      case (!result.includes(':')):
      result = protocol + '//' + hostname + '/' + result;
      break;

      //case (result.startsWith('http')):
      //break;
    }

    console.log(result)
    let url = new URL(result);
    let bol = url.pathname.endsWith(array[i-1]);
    if (bol) { createLink(result, type) };
  }
}


function extractRel(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    query = [
     // Also rel="feed". See https://miranda-ng.org/
      '//link[@rel="alternate"\
       and contains(@type, "' + array[i] + '")]\
       /@href'];
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) { createLink(result, type) };
}


function extractURI(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    query = [
      '//a[starts-with(@href, "' + array[i] + '")\
      and not(starts-with(@href, "mailto:?"))\
      and not(contains(@href, "/send?")\
      )]/@href'];
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) {
    let url = new URL(result);
    let bol = url.protocol.match(array[i-1]);
    if (bol) { createLink(result, type) };
  }
}


function extractURL(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    query = [
      '//a[starts-with(@href, "http")\
       and contains(@href, "' + array[i] + '")]\
       /@href'];
      // FIXME mainstream
      //'//a[starts-with(@href, "http") and ends-with(@href, "' + array[i] + '")]/@href'
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) {
    let url = new URL(result);
    let bol = url.hostname.endsWith(array[i-1]);
    if (bol) { createLink(result, type) };
    //if (!url) {
    //  url = url.host.contains(array[i] + ':');
    //}
  }
}


function extractURN(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    query = [
      '//a[starts-with(@href, "magnet")\
       and contains(@href, "' + array[i] + '")]\
       /@href'];
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) {
    let url = new URL(result);
    url.searchParams.delete('tr');
    result = url.protocol + url.search;
    result = decodeURIComponent(result);
    createLink(result, type)
    //let bol = url.hostname.startsWith(array[i-1]);
    //if (bol) { createLink(result, type) };
  }
}


function extractWeb(array, type) {
  if (checkID(type)) {return};
  let i = 0;

  do {
    query = [
      '//a[starts-with(@href, "http")\
       and contains(@href, "' + array[i] + '")\
       and not(contains(@href, "com.github.android"))\
       and not(contains(@href, "1477376905"))]\
       /@href'];
    result = executeQuery(query, 'xpath');
    i = i + 1;
  } while (!result && i < array.length);

  if (result) { createLink(result, type) };
}


function executeQuery(queries, method) {

  let i = 0;
  do {
    switch(method) {
      case 'css':
      result = document.querySelector(queries[i]);
      //if (result) {result = result.href};
      if (result) {return result.href};
      break;

      case 'xpath':
      result = document.evaluate(
        queries[i], document, null, XPathResult.STRING_TYPE);
      //if (result) {result = result.stringValue};
      if (result) {return result.stringValue};
    }
  } while (!result && i < queries.length);
}

function checkID(type) {
  for (let i = 0; i < eles.length; i++) {
    if (eles[i].id === type[3] + '_' + type[1] + '_OUJS') {
      return true;
    }
  }
}

function createLink(uri, type) {
  //if (type[4]) { 
  //let tip = document.createElement('spna');
  //tip.class = 'tooltip';
  //tip.append('type[4]');
  //}

  //type = type.split(' ');
  //sym = getUrnProperty(uri, 'sym');
  //net = getUrnProperty(uri, 'net');

  let ele = document.createElement('a');
  ele.append(type[1] + ' ' + type[3]);
  ele.href = uri;
  ele.id = type[3] + '_' + type[1] + '_OUJS';
  if (type[4]) { ele.title = type[4] }
  ele.style.color = '#eee';
  ele.style.font = 'caption';
  ele.style.fontFamily = 'arial';
  ele.style.fontSize = '15px'; // 13px
  ele.style.fontVariantCaps = 'all-small-caps';
  ele.style.padding = '3px 9px 3px 9px';
  ele.style.textDecoration = 'none';

  //ele.append(tip);

  console.log(ele)
  console.log(eles)
  eles.push(ele);
}

// TODO if eles[i].id does not exist
types.forEach(type => determineType(type));


// Torrent V1
// TODO handle compressed sha1 http://www.debath.co.uk/MakeAKey.html
// TODO convert base32 to hash
// 32/40 https://linuxtracker.org/?page=torrent-details&id=173a0f61ef92b158547937fa0c01e9dc704779f9
function generateTorrent() {
  for (let i = 0; i < eles.length; i++) {
    if (eles[i].id === 'Torrent_πŸ“¦οΈ_OUJS') {
      return
      // TODO generate link else-if onclick
      // 404 https://bookshelf.theanarchistlibrary.org/library/librarian-previous-announcements-en#toc1
    } else {
      if (eles[i].id === 'BitTorrent_🌊️_OUJS') {
        href = eles[i].href;
        let url = new URL(href);
        name = url.searchParams.get('dn');
        if (!name) {name = document.title};
        //xt = url.searchParams.get('xt');
        hash = url.searchParams.get('xt').slice(9);
        //if (ha.length === 40 && xt.startsWith('urn:btih'))
        if (hash.length === 40) {
          links = [
            'https://watercache.libertycorp.org/get/' + hash + '/' + name,
            'https://itorrents.org/torrent/' + hash + '.torrent?title=' + name,
            'https://firecache.libertycorp.org/get/' + hash + '/' + name,
            'http://fcache63sakpihd44kxdduy6kgpdhgejgp323wci435zwy6kiylcnfad.onion/get/' + hash + '/' + name,
            ];
          //type = types.findIndex(element => element === 'ext πŸ“¦οΈ torrent TORRENT');
          //type = type.split(' ');
          type = 'ext,πŸ“¦οΈ,torrent,TORRENT'; //🧧️ //🎁️
          type = type.split(',');
          console.log('links[1]')
          console.log(links[1])
          console.log('result')
          console.log(result)
          createLink(links[1], type)
        }
      }
    }
  }
}


if (eles.length > 0) {
  let bar = document.createElement('div');
  bar.className = 'media-bar';
  bar.id = 'org.openuserjs.sjehuda.blackbelt';
  bar.style.opacity = 0.75;
  bar.style.backgroundColor = '#000'; //'#2c3e50';
  bar.style.color = '#eee';
  //bar.style.setProperty("color", "#eee", "!important")
  bar.style.fontVariant = 'small-caps';
  bar.style.left = 0;
  bar.style.right = 0;
  bar.style.top = 0;
  bar.style.zIndex = 10000000000;
  bar.style.padding = '6px'; //13px //15px //11px //9px //6px //3px //1px
  bar.style.position = 'fixed';
  bar.style.textAlign = 'center';
  bar.style.direction = 'ltr';
  bar.onclick = () => { document.querySelector(".media-bar").style.display = "none"; }
  
  const top = document.querySelector('body');
  top.prepend(bar);

  generateTorrent()

  eles.forEach(ele => bar.append(ele));
  console.log("eles.forEach(ele => bar.append(ele));")
  console.log(eles)

  // Timer from https://stackoverflow.com/questions/27406765/hide-div-after-x-amount-of-seconds

  var secs = 33;
  function timeOut() {
    secs -= 1;
    if ( secs==0 ) {
      bar.style.display = 'none';
      return;
    }
    else {
      setTimeout(timeOut, 1000);
    }
  }
  timeOut();
}