Twitter Direct

Remove t.co tracking links from Twitter

目前為 2020-06-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name          Twitter Direct
// @description   Remove t.co tracking links from Twitter
// @author        chocolateboy
// @copyright     chocolateboy
// @version       0.0.1
// @namespace     https://github.com/chocolateboy/userscripts
// @license       GPL: https://www.gnu.org/copyleft/gpl.html
// @include       https://twitter.com/
// @include       https://twitter.com/*
// @run-at        document-start
// @inject-into   content
// ==/UserScript==

/*
 * scan a JSON response for tweets if its URL matches this pattern
 */
const PATTERN = /^https:\/\/api\.twitter\.com\/([^/]+\/[^.]+\.json)\?/

/*
 * compatibility shim needed for Violentmonkey:
 * https://github.com/violentmonkey/violentmonkey/issues/997#issuecomment-637700732
 */
const Compat = {}

/*
 * replace t.co URLs with the original URL in all locations within the document
 * which contain tweets
 */
function transformLinks (path, data) {
    const objects = data.globalObjects || data.twitter_objects
    const tweets = objects ? objects.tweets : objects

    if (!tweets) {
        console.debug("can't find tweets in:", path)
        return
    }

    console.info(`scanning links in ${path}:`, data)

    for (const tweet of Object.values(tweets)) {
        const { entities, extended_entities = {} } = tweet
        const { urls = [], media = [] } = entities
        const { urls: extendedUrls = [], media: extendedMedia = [] } = extended_entities
        const locations = [urls, media, extendedUrls, extendedMedia]

        for (const location of locations) {
            for (const item of location) {
                item.url = item.expanded_url
            }
        }
    }

    return data
}

/*
 * parse and transform the JSON response, handling (catching and reporting) any
 * parse or processing errors
 */
function transformResponse (json, path) {
    let parsed

    try {
        parsed = JSON.parse(json)
    } catch (e) {
        console.error("Can't parse response:", e)
        return
    }

    let transformed

    try {
        transformed = transformLinks(path, parsed)
    } catch (e) {
        console.error('error transforming JSON:', e)
        return
    }

    return transformed
}

/*
 * replacement for Twitter's default response handler which transforms the
 * response if it's a) JSON and b) contains tweet data; otherwise, we leave it
 * unchanged
 */
function onReadyStateChange (xhr, url) {
    const match = url.match(PATTERN)

    if (!match) {
        console.debug("can't match URL:", url)
        return
    }

    const path = match[1]
    const transformed = transformResponse(xhr.responseText, path)

    if (transformed) {
        const descriptor = { value: JSON.stringify(transformed) }
        const clone = Compat.cloneInto(descriptor, Compat.unsafeWindow)
        Compat.unsafeWindow.Object.defineProperty(xhr, 'responseText', clone)
    }
}

/*
 * replace the built-in XHR#send method with our custom version which swaps in
 * our custom response handler. once done, we delegate to the original handler
 * (this.onreadystatechange)
 */
function hookXHRSend (oldSend) {
    return function send () {
        const oldOnReadyStateChange = this.onreadystatechange

        this.onreadystatechange = function () {
            if (this.readyState === 4 && this.responseURL && this.status === 200) {
                onReadyStateChange(this, this.responseURL)
            }

            oldOnReadyStateChange.apply(this, arguments)
        }

        return oldSend.apply(this, arguments)
    }
}

/*
 * set up a cross-engine API to shield us from differences between engines so we
 * don't have to clutter the code with conditionals.
 *
 * XXX the functions are only needed for Violentmonkey, and are effectively
 * no-ops in other engines
 */
if (GM_info.scriptHandler === 'Violentmonkey') {
    Compat.unsafeWindow = unsafeWindow.wrappedJSObject
    Compat.cloneInto = cloneInto
    Compat.exportFunction = exportFunction
} else {
    Compat.cloneInto = Compat.exportFunction = value => value
    Compat.unsafeWindow = unsafeWindow
}

console.debug('hooking XHR#send:', Compat.unsafeWindow.XMLHttpRequest.prototype.send)

/*
 * replace the default XHR#send with our custom version, which scans responses
 * for tweets and expands their URLs
 */

Compat.unsafeWindow.XMLHttpRequest.prototype.send = Compat.exportFunction(
    hookXHRSend(window.XMLHttpRequest.prototype.send),
    Compat.unsafeWindow
)