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!');