Twitter Direct

Remove t.co tracking links from Twitter

当前为 2020-06-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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
)