您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Remove t.co tracking links from Twitter
- // ==UserScript==
- // @name Twitter Direct
- // @description Remove t.co tracking links from Twitter
- // @author chocolateboy
- // @copyright chocolateboy
- // @version 3.1.2
- // @namespace https://github.com/chocolateboy/userscripts
- // @license GPL
- // @include https://mobile.twitter.com/
- // @include https://mobile.twitter.com/*
- // @include https://twitter.com/
- // @include https://twitter.com/*
- // @include https://x.com/
- // @include https://x.com/*
- // @require https://unpkg.com/gm-compat@1.1.0/dist/index.iife.min.js
- // @run-at document-start
- // ==/UserScript==
- // NOTE This file is generated from src/twitter-direct.user.ts and should not be edited directly.
- "use strict";
- (() => {
- // src/twitter-direct/util.ts
- var isObject = (value) => !!value && typeof value === "object";
- var isPlainObject = function() {
- const toString = {}.toString;
- return (value) => toString.call(value) === "[object Object]";
- }();
- var typeOf = (value) => value === null ? "null" : typeof value;
- var isType = (type) => {
- return (value) => {
- return typeOf(value) === type;
- };
- };
- var isString = isType("string");
- var isNumber = isType("number");
- // src/twitter-direct/replacer.ts
- var DOCUMENT_ROOTS = [
- "data",
- "globalObjects",
- "inbox_initial_state",
- "users"
- ];
- var LEGACY_KEYS = [
- "binding_values",
- "entities",
- "extended_entities",
- "full_text",
- "lang",
- "quoted_status_permalink",
- "retweeted_status",
- "retweeted_status_result",
- "user_refs"
- ];
- var PRUNE_KEYS = /* @__PURE__ */ new Set([
- "advertiser_account_service_levels",
- "card_platform",
- "clientEventInfo",
- "ext",
- "ext_media_color",
- "features",
- "feedbackInfo",
- "hashtags",
- "indices",
- "original_info",
- "player_image_color",
- "profile_banner_extensions",
- "profile_banner_extensions_media_color",
- "profile_image_extensions",
- "profile_image_extensions_media_color",
- "responseObjects",
- "sizes",
- "user_mentions",
- "video_info"
- ]);
- var checkUrl = /* @__PURE__ */ function() {
- const urlPattern = /^https?:\/\/\w/i;
- return (value) => urlPattern.test(value) && value;
- }();
- var isTrackedUrl = /* @__PURE__ */ function() {
- const urlPattern = /^https?:\/\/t\.co\/\w+$/;
- return (value) => urlPattern.test(value);
- }();
- var isURLData = (value) => {
- return isPlainObject(value) && isString(value.url) && isString(value.expanded_url) && Array.isArray(value.indices) && isNumber(value.indices[0]) && isNumber(value.indices[1]);
- };
- var Replacer = class _Replacer {
- seen = /* @__PURE__ */ new Map();
- unresolved = /* @__PURE__ */ new Map();
- count = 0;
- static transform(data, path) {
- const replacer = new _Replacer();
- return replacer.transform(data, path);
- }
- /*
- * replace t.co URLs with the original URL in all locations in the document
- * which may contain them
- *
- * returns the number of substituted URLs
- */
- transform(data, path) {
- const { seen, unresolved } = this;
- if (Array.isArray(data) || "id_str" in data) {
- this.traverse(data);
- } else {
- for (const key of DOCUMENT_ROOTS) {
- if (key in data) {
- this.traverse(data[key]);
- }
- }
- }
- for (const [url, targets] of unresolved) {
- const expandedUrl = seen.get(url);
- if (expandedUrl) {
- for (const { target, key } of targets) {
- target[key] = expandedUrl;
- ++this.count;
- }
- unresolved.delete(url);
- }
- }
- if (unresolved.size) {
- console.warn(`unresolved URIs (${path}):`, Object.fromEntries(unresolved));
- }
- return this.count;
- }
- /*
- * reduce the large binding_values array/object to the one property we care
- * about (card_url)
- */
- onBindingValues(value) {
- if (Array.isArray(value)) {
- const found = value.find((it) => it?.key === "card_url");
- return found ? [found] : 0;
- } else if (isPlainObject(value) && isPlainObject(value.card_url)) {
- return [value.card_url];
- } else {
- return 0;
- }
- }
- /*
- * handle cases where the t.co URL is already expanded, e.g.:
- *
- * {
- * "entities": {
- * "urls": [
- * {
- * "display_url": "example.com",
- * "expanded_url": "https://www.example.com",
- * "url": "https://www.example.com",
- * "indices": [16, 39]
- * }
- * ]
- * },
- * "full_text": "I'm on the bus! https://t.co/abcde12345"
- * }
- *
- * extract the corresponding t.co URLs from the text via the entities.urls
- * records and register the t.co -> expanded URL mappings so they can be
- * used later, e.g. https://t.co/abcde12345 -> https://www.example.com
- */
- onFullText(context, message) {
- const seen = this.seen;
- const urls = context.entities?.urls;
- if (!(Array.isArray(urls) && urls.length)) {
- return message;
- }
- const $message = Array.from(message);
- for (let i = 0; i < urls.length; ++i) {
- const $url = urls[i];
- if (!isURLData($url)) {
- break;
- }
- const {
- url,
- expanded_url: expandedUrl,
- indices: [start, end]
- } = $url;
- const alreadyExpanded = !isTrackedUrl(url) && expandedUrl === url;
- if (!alreadyExpanded) {
- continue;
- }
- const trackedUrl = context.lang === "zxx" ? message : $message.slice(start, end).join("");
- seen.set(trackedUrl, expandedUrl);
- }
- return message;
- }
- /*
- * reduce the keys under $.legacy (typically around 30) to the
- * handful we care about
- */
- onLegacyObject(value) {
- const filtered = {};
- for (let i = 0; i < LEGACY_KEYS.length; ++i) {
- const key = LEGACY_KEYS[i];
- if (key in value) {
- filtered[key] = value[key];
- }
- }
- return filtered;
- }
- /*
- * expand t.co URL nodes in place, either $.url or $.string_value in
- * binding_values arrays/objects
- */
- onTrackedURL(context, key, url) {
- const { seen, unresolved } = this;
- let expandedUrl;
- if (expandedUrl = seen.get(url)) {
- context[key] = expandedUrl;
- ++this.count;
- } else if (expandedUrl = checkUrl(context.expanded_url || context.expanded)) {
- seen.set(url, expandedUrl);
- context[key] = expandedUrl;
- ++this.count;
- } else {
- let targets = unresolved.get(url);
- if (!targets) {
- unresolved.set(url, targets = []);
- }
- targets.push({ target: context, key });
- }
- return url;
- }
- /*
- * traverse an object by hijacking JSON.stringify's visitor (replacer).
- * dispatches each node to the +visit+ method
- */
- traverse(data) {
- if (!isObject(data)) {
- return;
- }
- const self = this;
- const replacer = function(key, value) {
- return Array.isArray(this) ? value : self.visit(this, key, value);
- };
- JSON.stringify(data, replacer);
- }
- /*
- * visitor callback which replaces a t.co +url+ property in an object with
- * its expanded URL
- */
- visit(context, key, value) {
- if (PRUNE_KEYS.has(key)) {
- return 0;
- }
- switch (key) {
- case "binding_values":
- return this.onBindingValues(value);
- case "full_text":
- if (isString(value)) {
- return this.onFullText(context, value);
- }
- break;
- case "legacy":
- if (isPlainObject(value)) {
- return this.onLegacyObject(value);
- }
- break;
- case "string_value":
- case "url":
- if (isTrackedUrl(value)) {
- return this.onTrackedURL(context, key, value);
- }
- break;
- }
- return value;
- }
- };
- var replacer_default = Replacer;
- // src/twitter-direct.user.ts
- // @license GPL
- var URL_BLACKLIST = /* @__PURE__ */ new Set([
- "/hashflags.json",
- "/badge_count/badge_count.json",
- "/graphql/articleNudgeDomains",
- "/graphql/TopicToFollowSidebar"
- ]);
- var CONTENT_TYPE = /^application\/json\b/;
- var LOG_THRESHOLD = 1024;
- var STATS = {};
- var TWITTER_API = /^(?:(?:api|mobile)\.)?(?:twitter|x)\.com$/;
- var onResponse = (xhr, uri) => {
- const contentType = xhr.getResponseHeader("Content-Type");
- if (!contentType || !CONTENT_TYPE.test(contentType)) {
- return;
- }
- const url = new URL(uri);
- if (!TWITTER_API.test(url.hostname)) {
- return;
- }
- const json = xhr.responseText;
- const size = json.length;
- const path = url.pathname.replace(/^\/i\/api\//, "/").replace(/^\/\d+(\.\d+)*\//, "/").replace(/(\/graphql\/)[^\/]+\/(.+)$/, "$1$2").replace(/\/\d+\.json$/, ".json");
- if (URL_BLACKLIST.has(path)) {
- return;
- }
- let data;
- try {
- data = JSON.parse(json);
- } catch (e) {
- console.error(`Can't parse JSON for ${uri}:`, e);
- return;
- }
- if (!isObject(data)) {
- return;
- }
- const newPath = !(path in STATS);
- const count = replacer_default.transform(data, path);
- STATS[path] = (STATS[path] || 0) + count;
- if (!count) {
- if (STATS[path] === 0 && size > LOG_THRESHOLD) {
- console.debug(`no replacements in ${path} (${size} B)`);
- }
- return;
- }
- const descriptor = { value: JSON.stringify(data) };
- const clone = GMCompat.export(descriptor);
- GMCompat.unsafeWindow.Object.defineProperty(xhr, "responseText", clone);
- const replacements = "replacement" + (count === 1 ? "" : "s");
- console.debug(`${count} ${replacements} in ${path} (${size} B)`);
- if (newPath) {
- console.log(STATS);
- }
- };
- var hookXHRSend = (oldSend) => {
- return function send2(body = null) {
- const oldOnReadyStateChange = this.onreadystatechange;
- this.onreadystatechange = function(event) {
- if (this.readyState === this.DONE && this.responseURL && this.status === 200) {
- onResponse(this, this.responseURL);
- }
- if (oldOnReadyStateChange) {
- oldOnReadyStateChange.call(this, event);
- }
- };
- oldSend.call(this, body);
- };
- };
- var xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
- var send = hookXHRSend(xhrProto.send);
- xhrProto.send = GMCompat.export(send);
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址