NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Skiptco // @namespace https://github.com/albancrommer/skiptco // @description Replaces Twitter t.co redirection by original links // @version 2.0.3 // @author openuserjsalbancrommer.com // @match https://twitter.com/* // @match https://tweetdeck.twitter.com/* // @license GPL v3 // @updateURL https://openuserjs.org/meta/openuserjsalbancrommer.com/Skiptco.meta.js // @icon http://www.crommer.net/skiptco/icon-small.png // @run-at document-end // @homepageURL https://github.com/albancrommer/skiptco // ==/UserScript== (function(){ // Script wide settings and vars var DEBUG = false, agent = {}, bind_class = "tco-bind", // class tagging observed DOM elements anchor_class = "tco-ham-proof", // class for tagging modified anchors anchor_class_nomark = "tco-ham-nomark", // class for tagging anchors with no mark doc; // singleton for the document /** * Provides poor-man console.log alternative * @returns {undefined} */ function _debug (){ var a = arguments, d = "_dbg", i = 0, l = arguments.length, m = "" ; // Skips if deactivated if (!DEBUG) { return; } // Skips if no args if (0===l) { return; } // Logs for (i=0; i<l; i++) { m+=a[i]+" "; } d = new Date(); m = d.getSeconds() +":"+d.getMilliseconds()+ " " + m; console.log( m); } /** * Defines the agent template * * @param {type} observedItemList * @returns {skiptcoAgent.instance} */ skiptcoAgent = function (observedItemList){ /** * The actual agent * */ var instance = { /** * Performs DOM binding to the agent * * @returns {undefined} */ init: function() { // Retrieves the active tab document doc = window.document; // Adds style to page var style = doc.createElement("style"); style.innerHTML = '.tco-ham-proof:before{ content: "↷";}'; doc.body.appendChild(style); // Binds to click doc.body.addEventListener("click",function(){agent.run();},false); MutationObserver = window.MutationObserver || window.WebKitMutationObserver; var observer = new MutationObserver(function(mutations, observer) { agent.run(); }); observer.observe(doc, { subtree: true, attributes: true }); }, /** * * @param {type} selector * @returns {undefined} */ patchURL: function ( selector ){ // Defines the injected class name var cls = anchor_class, el, // element efc, // element first child ham, // the Good Link i, // iterator list = doc.querySelectorAll(selector), // Elements list r = /(http\S*)/, // Regular Expression for links cleaning rr, // Regular Expression result text // Element text content (some links have no data attribute) ; for (i = 0; i<list.length;i++ ){ el = list[i] if(el.classList.contains(cls)){ continue; } // Builds ham, either with data attribute or current anchor text text = el.textContent.trim(); ham = text ; if( el.hasAttribute("data-full-url") ) { ham = el.getAttribute("data-full-url") ; }else if( el.hasAttribute("data-expanded-url" ) ) { ham = el.getAttribute("data-expanded-url") } // It should not replace some internal links if( ham[0] === "@" ){ continue; } rr = r.exec(ham); _debug("before : ham= "+ham+" text="+text) _debug("rr before: "+rr) ham = ( null !== rr ) ? rr[1]:"http://"+ham _debug("ham after: "+ham) // Adds no class for certain objects (thumbnails) efc = el.firstElementChild; if( undefined !== efc && null !== efc){ if( efc.classList.contains("summary-thumbnail")){ continue; } } el.href = ham; el.target = "_blank"; el.classList.add(cls); } }, closest : function(el, selector, stopSelector) { var retval = null; while (el) { if (el.matches(selector)) { retval = el; break } else if (stopSelector && el.matches(stopSelector)) { break } el = el.parentElement; } return retval; }, /** * Patches twitter cards */ patchCards : function(){ var cards = document.getElementsByClassName('card2') for ( var i = 0; i < cards.length; i++ ){ // It should skip if the item has been tagged var card = cards[i] if( card.classList.contains(anchor_class)){ continue; } // It should find the iframe var iframe = card.getElementsByTagName("iframe")[0] // IT should skip if no iframe found if( ! iframe ){ break; } // It should find the iframe document var doc = iframe.contentWindow.document; if( ! doc ) { continue; } // It should find the ham link var cardParent = this.closest(card,".tweet"); var hamList = cardParent.querySelectorAll('[data-expanded-url]'); if( 0 === hamList.length ){ continue; } var ham_link = hamList[0].attributes["data-expanded-url"].value; // IT should find the links in the iframe document var aList = doc.getElementsByTagName("a") if( ! aList.length ){ continue; } // It should tag the card as done once the iframe document links are found card.classList.add(anchor_class); // It should send a message to the iframe for ( var i = 0; i < aList.length ; i ++){ var el = aList[i]; el.href=ham_link; } } } }; return instance; }; // Instanciates tweeter agent var twitterAgent = new skiptcoAgent([".stream-items","#profile_popup"]); /** * $(".promoted-account").hide() * $(".promoted-trend").hide() */ twitterAgent.hidePromotedElements = function (){ var el = doc.querySelectorAll("[class*='promoted']"), i; if( ! el.length ){ _debug("nothing to hide!"); return; } for (i=0; i<el.length; i++){ el[i].parentNode.removeChild(el[i]); } _debug("Hiding!"); }; /** * */ twitterAgent.run = function (){ this.patchURL("a.twitter-timeline-link"); this.patchCards(); // Skips promoted elements in a not so efficient way // @TODO: Make it efficient : ie add specific mutation listener setTimeout( function(){ agent.hidePromotedElements() } ,2000); }; // Tweetdeck agent var tweetdeckAgent = new skiptcoAgent([".js-app-content",".js-modal-content"]); /** * */ tweetdeckAgent.run = function(){ this.patchURL("a.url-ext"); }; // GreaseMonkey style : execute immediately if( /.*tweetdeck.*/.test(document.location.origin) ){ agent = tweetdeckAgent; }else{ agent = twitterAgent; } // Run the agent, who will bind itself to various elements agent.init(); agent.run(); })();