TweetDeck Direct

Remove t.co tracking links from TweetDeck

当前为 2022-06-26 提交的版本,查看 最新版本

  1. "use strict";
  2.  
  3. // ==UserScript==
  4. // @name TweetDeck Direct
  5. // @description Remove t.co tracking links from TweetDeck
  6. // @author chocolateboy
  7. // @copyright chocolateboy
  8. // @version 2.0.0
  9. // @namespace https://github.com/chocolateboy/userscripts
  10. // @license GPL
  11. // @include https://tweetdeck.twitter.com/
  12. // @include https://tweetdeck.twitter.com/*
  13. // @require https://unpkg.com/gm-compat@1.1.0/dist/index.iife.min.js
  14. // @run-at document-start
  15. // ==/UserScript==
  16.  
  17. // NOTE This file is generated from src/tweetdeck-direct.user.ts and should not be edited directly.
  18.  
  19. (() => {
  20. // src/twitter-direct/util.ts
  21. var checkUrl = function() {
  22. const urlPattern = /^https?:\/\/\w/i;
  23. return (value) => urlPattern.test(value) && value;
  24. }();
  25. var isObject = (value) => !!value && typeof value === "object";
  26. var isPlainObject = function() {
  27. const toString = {}.toString;
  28. return (value) => toString.call(value) === "[object Object]";
  29. }();
  30. var isTrackedUrl = function() {
  31. const urlPattern = /^https?:\/\/t\.co\/\w+$/;
  32. return (value) => urlPattern.test(value);
  33. }();
  34.  
  35. // src/twitter-direct/transformer.ts
  36. var CONTENT_TYPE = /^application\/json\b/;
  37. var DOCUMENT_ROOTS = [
  38. "data",
  39. "globalObjects",
  40. "inbox_initial_state",
  41. "modules",
  42. "users"
  43. ];
  44. var LEGACY_KEYS = [
  45. "binding_values",
  46. "entities",
  47. "extended_entities",
  48. "quoted_status_permalink",
  49. "retweeted_status",
  50. "retweeted_status_result",
  51. "user_refs"
  52. ];
  53. var LOG_THRESHOLD = 1024;
  54. var PRUNE_KEYS = /* @__PURE__ */ new Set([
  55. "advertiser_account_service_levels",
  56. "card_platform",
  57. "clientEventInfo",
  58. "ext",
  59. "ext_media_color",
  60. "features",
  61. "feedbackInfo",
  62. "hashtags",
  63. "indices",
  64. "original_info",
  65. "player_image_color",
  66. "profile_banner_extensions",
  67. "profile_banner_extensions_media_color",
  68. "profile_image_extensions",
  69. "profile_image_extensions_media_color",
  70. "responseObjects",
  71. "sizes",
  72. "user_mentions",
  73. "video_info"
  74. ]);
  75. var STATS = {};
  76. var TWITTER_API = /^(?:(?:api|mobile)\.)?twitter\.com$/;
  77. var URL_KEYS = /* @__PURE__ */ new Set(["url", "string_value"]);
  78. var Transformer = class {
  79. urlBlacklist;
  80. static register(options) {
  81. const transformer = new this(options);
  82. const xhrProto = GMCompat.unsafeWindow.XMLHttpRequest.prototype;
  83. const send = transformer.hookXHRSend(xhrProto.send);
  84. xhrProto.send = GMCompat.export(send);
  85. return transformer;
  86. }
  87. constructor(options) {
  88. this.urlBlacklist = options.urlBlacklist || /* @__PURE__ */ new Set();
  89. }
  90. onResponse(xhr, uri) {
  91. const contentType = xhr.getResponseHeader("Content-Type");
  92. if (!contentType || !CONTENT_TYPE.test(contentType)) {
  93. return;
  94. }
  95. const url = new URL(uri);
  96. if (!TWITTER_API.test(url.hostname)) {
  97. return;
  98. }
  99. const json = xhr.responseText;
  100. const size = json.length;
  101. const path = url.pathname.replace(/^\/i\/api\//, "/").replace(/^\/\d+(\.\d+)*\//, "/").replace(/(\/graphql\/)[^\/]+\/(.+)$/, "$1$2").replace(/\/\d+\.json$/, ".json");
  102. if (this.urlBlacklist.has(path)) {
  103. return;
  104. }
  105. let data;
  106. try {
  107. data = JSON.parse(json);
  108. } catch (e) {
  109. console.error(`Can't parse JSON for ${uri}:`, e);
  110. return;
  111. }
  112. if (!isObject(data)) {
  113. return;
  114. }
  115. const newPath = !(path in STATS);
  116. const count = this.transform(data, path);
  117. STATS[path] = (STATS[path] || 0) + count;
  118. if (!count) {
  119. if (!STATS[path] && size > LOG_THRESHOLD) {
  120. console.debug(`no replacements in ${path} (${size} B)`);
  121. }
  122. return;
  123. }
  124. const descriptor = { value: JSON.stringify(data) };
  125. const clone = GMCompat.export(descriptor);
  126. GMCompat.unsafeWindow.Object.defineProperty(xhr, "responseText", clone);
  127. const replacements = "replacement" + (count === 1 ? "" : "s");
  128. console.debug(`${count} ${replacements} in ${path} (${size} B)`);
  129. if (newPath) {
  130. console.log(STATS);
  131. }
  132. }
  133. transform(data, path) {
  134. const seen = /* @__PURE__ */ new Map();
  135. const unresolved = /* @__PURE__ */ new Map();
  136. const state = { count: 0, seen, unresolved };
  137. if (Array.isArray(data) || "id_str" in data) {
  138. this.traverse(state, data);
  139. } else {
  140. for (const key of DOCUMENT_ROOTS) {
  141. if (key in data) {
  142. this.traverse(state, data[key]);
  143. }
  144. }
  145. }
  146. for (const [url, targets] of unresolved) {
  147. const expandedUrl = seen.get(url);
  148. if (expandedUrl) {
  149. for (const { target, key } of targets) {
  150. target[key] = expandedUrl;
  151. ++state.count;
  152. }
  153. unresolved.delete(url);
  154. }
  155. }
  156. if (unresolved.size) {
  157. console.warn(`unresolved URIs (${path}):`, Object.fromEntries(state.unresolved));
  158. }
  159. return state.count;
  160. }
  161. transformBindingValues(value) {
  162. if (Array.isArray(value)) {
  163. const found = value.find((it) => it?.key === "card_url");
  164. return found ? [found] : 0;
  165. } else if (isPlainObject(value)) {
  166. return { card_url: value.card_url || 0 };
  167. } else {
  168. return 0;
  169. }
  170. }
  171. transformLegacyObject(value) {
  172. const filtered = {};
  173. for (let i = 0; i < LEGACY_KEYS.length; ++i) {
  174. const key = LEGACY_KEYS[i];
  175. if (key in value) {
  176. filtered[key] = value[key];
  177. }
  178. }
  179. return filtered;
  180. }
  181. transformURL(state, context, key, value) {
  182. const { seen, unresolved } = state;
  183. const writable = this.isWritable(context);
  184. let expandedUrl;
  185. if (expandedUrl = seen.get(value)) {
  186. if (writable) {
  187. context[key] = expandedUrl;
  188. ++state.count;
  189. }
  190. } else if (expandedUrl = checkUrl(context.expanded_url || context.expanded)) {
  191. seen.set(value, expandedUrl);
  192. if (writable) {
  193. context[key] = expandedUrl;
  194. ++state.count;
  195. }
  196. } else {
  197. let targets = unresolved.get(value);
  198. if (!targets) {
  199. unresolved.set(value, targets = []);
  200. }
  201. if (writable) {
  202. targets.push({ target: context, key });
  203. }
  204. }
  205. }
  206. hookXHRSend(oldSend) {
  207. const self = this;
  208. return function send(body = null) {
  209. const oldOnReadyStateChange = this.onreadystatechange;
  210. this.onreadystatechange = function(event) {
  211. if (this.readyState === this.DONE && this.responseURL && this.status === 200) {
  212. self.onResponse(this, this.responseURL);
  213. }
  214. if (oldOnReadyStateChange) {
  215. oldOnReadyStateChange.call(this, event);
  216. }
  217. };
  218. oldSend.call(this, body);
  219. };
  220. }
  221. isWritable(_context) {
  222. return true;
  223. }
  224. traverse(state, data) {
  225. if (!isObject(data)) {
  226. return;
  227. }
  228. const self = this;
  229. const replacer = function(key, value) {
  230. return Array.isArray(this) ? value : self.visit(state, this, key, value);
  231. };
  232. JSON.stringify(data, replacer);
  233. }
  234. visit(state, context, key, value) {
  235. if (PRUNE_KEYS.has(key)) {
  236. return 0;
  237. }
  238. if (key === "binding_values") {
  239. return this.transformBindingValues(value);
  240. }
  241. if (key === "legacy" && isPlainObject(value)) {
  242. return this.transformLegacyObject(value);
  243. }
  244. if (URL_KEYS.has(key) && isTrackedUrl(value)) {
  245. this.transformURL(state, context, key, value);
  246. }
  247. return value;
  248. }
  249. };
  250.  
  251. // src/tweetdeck-direct.user.ts
  252. // @license GPL
  253. var INIT = { childList: true, subtree: true };
  254. var SELECTOR = "a[href][data-full-url]:not([data-fixed])";
  255. var URL_BLACKLIST = /* @__PURE__ */ new Set([
  256. "/search/typeahead.json",
  257. "/trends/available.json",
  258. "/blocks/ids.json",
  259. "/lists/ownerships.json",
  260. "/mutes/users/ids.json",
  261. "/tweetdeck/clients/blackbird/all",
  262. "/account/verify_credentials.json",
  263. "/trends/plus.json",
  264. "/collections/list.json",
  265. "/help/settings.json"
  266. ]);
  267. var Transformer2 = class extends Transformer {
  268. isWritable(context) {
  269. return !("indices" in context);
  270. }
  271. };
  272. var run = () => {
  273. const target = document.body;
  274. const replace = () => {
  275. for (const link of target.querySelectorAll(SELECTOR)) {
  276. link.href = link.dataset.fullUrl;
  277. link.dataset.fixed = "true";
  278. }
  279. };
  280. replace();
  281. new MutationObserver(replace).observe(target, INIT);
  282. };
  283. window.addEventListener("DOMContentLoaded", run);
  284. Transformer2.register({ urlBlacklist: URL_BLACKLIST });
  285. })();