NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name unlink.is // @namespace http://gmscript.gentoo.moe // @include https://twitter.com/* // @include https://tweetdeck.twitter.com/* // @exclude https://twitter.com/i/* // @version 0.4 // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js // @downloadURL https://raw.githubusercontent.com/perillamint/unlink.is/master/unlinkis.js // @updateURL https://raw.githubusercontent.com/perillamint/unlink.is/master/unlinkis.meta.js // @grant GM_xmlhttpRequest // @license MPL-2.0 or later // @connect t.co // @connect ln.is // @connect linkis.com // ==/UserScript== //Caution: This parsing method is not code-change resist. var jsblock_regex = /<script[^>]*>[\s\S]([.\s\S]*?)<\/script>/g; var urlgrabber_regex = /longUrl"?:[ ]*"(.+?)"/; var linkdata_obj_regex = /var LinkData/; var linkis_detect = /(?:ln\.is|linkis\.com)\/\w+/; var linkis_card_detect = /(?:ln\.is|linkis\.com)/; var card_iframe_regex = /xdm_default\d+?_provider/; //Caution: This url extractor WILL CRASH when twitter changes working method of t.co var tco_url_extract_regex = /location\.replace\("(.*?)"\)/; var tco_url_detect = /http(|s):\/\/t.co\/[^\/]*$/; var tweetdeck = location.hostname === 'tweetdeck.twitter.com'; function get_jsblock(str) { var match = str.match(jsblock_regex); var ret = []; for (var i = 1; i < match.length; i++) { ret[i - 1] = match[i]; } return ret; } function extract_url(str) { if (str.match(linkdata_obj_regex) !== null) { var match = str.match(urlgrabber_regex); if (match !== null) { return match[1].replace(/\\\//g, '/'); } } return null; } function convert_and_patch(url, jqobj, depth) { if (depth > 100) return; console.log('Resolving url: ' + url); GM_xmlhttpRequest({ method: 'GET', url: url, onload: function(resp) { //Check xmlhttpRequest ends with t.co if (resp.finalUrl.match(tco_url_detect) !== null) { var url_arr = resp.response.match(tco_url_extract_regex); if (url_arr === null) return; convert_and_patch(url_arr[1].replace(/\\\//g, '/'), jqobj, depth + 1); } var jsblock_arr = get_jsblock(resp.response); var url = null; for (var i = 0; i < jsblock_arr.length; i++) { url = extract_url(jsblock_arr[i]); if (url !== null) break; } jqobj.attr('href', url); // Check `jqobj` is normal link or tweet card if (jqobj.hasClass('twitter-timeline-link') || jqobj.hasClass('url-ext')) { jqobj.text(url).attr('title', url); } else if (jqobj.hasClass('TwitterCard-container')) { jqobj.find('span.SummaryCard-destination').text(jqobj[0].hostname); } }, onerror: function(err) { console.log(err); } }); } function tweet_handler(elem) { if (elem.tagName == 'IFRAME' && card_iframe_regex.test(elem.id)) { $(elem).on('load', function (event) { var card_hostname = elem.contentWindow.document.querySelector('span.SummaryCard-destination'); if (card_hostname !== null && linkis_card_detect.test(card_hostname.textContent)) { var link = elem.contentWindow.document.querySelector('a.js-openLink'); convert_and_patch(link.href, $(link), 0); } }); } else { if (tweetdeck) { var links = $(elem).find('a.url-ext'); } else { var links = $(elem).find('a.twitter-timeline-link'); } for (var i = 0; i < links.length; i++) { var match = $(links[i]).text().match(linkis_detect); if (match !== null) { convert_and_patch(links[i].href, $(links[i]), 0); } } } } function get_tweets() { $('.tweet').each(function(i, tweet) { tweet_handler(tweet); }); } var obs_config = { childList: true, characterData: true, subtree: true }; var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { var added_nodes = mutation.addedNodes; for (var i = 0; i < added_nodes.length; i++) { tweet_handler(added_nodes[i]); } }); }); observer.observe(document.body, obs_config); get_tweets(); console.log('Unlink.is ready!');