Twitter Direct

Remove t.co tracking links from Twitter

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

  1. // ==UserScript==
  2. // @name Twitter Direct
  3. // @description Remove t.co tracking links from Twitter
  4. // @author chocolateboy
  5. // @copyright chocolateboy
  6. // @version 0.0.1
  7. // @namespace https://github.com/chocolateboy/userscripts
  8. // @license GPL: https://www.gnu.org/copyleft/gpl.html
  9. // @include https://twitter.com/
  10. // @include https://twitter.com/*
  11. // @run-at document-start
  12. // @inject-into content
  13. // ==/UserScript==
  14.  
  15. /*
  16. * scan a JSON response for tweets if its URL matches this pattern
  17. */
  18. const PATTERN = /^https:\/\/api\.twitter\.com\/([^/]+\/[^.]+\.json)\?/
  19.  
  20. /*
  21. * compatibility shim needed for Violentmonkey:
  22. * https://github.com/violentmonkey/violentmonkey/issues/997#issuecomment-637700732
  23. */
  24. const Compat = {}
  25.  
  26. /*
  27. * replace t.co URLs with the original URL in all locations within the document
  28. * which contain tweets
  29. */
  30. function transformLinks (path, data) {
  31. const objects = data.globalObjects || data.twitter_objects
  32. const tweets = objects ? objects.tweets : objects
  33.  
  34. if (!tweets) {
  35. console.debug("can't find tweets in:", path)
  36. return
  37. }
  38.  
  39. console.info(`scanning links in ${path}:`, data)
  40.  
  41. for (const tweet of Object.values(tweets)) {
  42. const { entities, extended_entities = {} } = tweet
  43. const { urls = [], media = [] } = entities
  44. const { urls: extendedUrls = [], media: extendedMedia = [] } = extended_entities
  45. const locations = [urls, media, extendedUrls, extendedMedia]
  46.  
  47. for (const location of locations) {
  48. for (const item of location) {
  49. item.url = item.expanded_url
  50. }
  51. }
  52. }
  53.  
  54. return data
  55. }
  56.  
  57. /*
  58. * parse and transform the JSON response, handling (catching and reporting) any
  59. * parse or processing errors
  60. */
  61. function transformResponse (json, path) {
  62. let parsed
  63.  
  64. try {
  65. parsed = JSON.parse(json)
  66. } catch (e) {
  67. console.error("Can't parse response:", e)
  68. return
  69. }
  70.  
  71. let transformed
  72.  
  73. try {
  74. transformed = transformLinks(path, parsed)
  75. } catch (e) {
  76. console.error('error transforming JSON:', e)
  77. return
  78. }
  79.  
  80. return transformed
  81. }
  82.  
  83. /*
  84. * replacement for Twitter's default response handler which transforms the
  85. * response if it's a) JSON and b) contains tweet data; otherwise, we leave it
  86. * unchanged
  87. */
  88. function onReadyStateChange (xhr, url) {
  89. const match = url.match(PATTERN)
  90.  
  91. if (!match) {
  92. console.debug("can't match URL:", url)
  93. return
  94. }
  95.  
  96. const path = match[1]
  97. const transformed = transformResponse(xhr.responseText, path)
  98.  
  99. if (transformed) {
  100. const descriptor = { value: JSON.stringify(transformed) }
  101. const clone = Compat.cloneInto(descriptor, Compat.unsafeWindow)
  102. Compat.unsafeWindow.Object.defineProperty(xhr, 'responseText', clone)
  103. }
  104. }
  105.  
  106. /*
  107. * replace the built-in XHR#send method with our custom version which swaps in
  108. * our custom response handler. once done, we delegate to the original handler
  109. * (this.onreadystatechange)
  110. */
  111. function hookXHRSend (oldSend) {
  112. return function send () {
  113. const oldOnReadyStateChange = this.onreadystatechange
  114.  
  115. this.onreadystatechange = function () {
  116. if (this.readyState === 4 && this.responseURL && this.status === 200) {
  117. onReadyStateChange(this, this.responseURL)
  118. }
  119.  
  120. oldOnReadyStateChange.apply(this, arguments)
  121. }
  122.  
  123. return oldSend.apply(this, arguments)
  124. }
  125. }
  126.  
  127. /*
  128. * set up a cross-engine API to shield us from differences between engines so we
  129. * don't have to clutter the code with conditionals.
  130. *
  131. * XXX the functions are only needed for Violentmonkey, and are effectively
  132. * no-ops in other engines
  133. */
  134. if (GM_info.scriptHandler === 'Violentmonkey') {
  135. Compat.unsafeWindow = unsafeWindow.wrappedJSObject
  136. Compat.cloneInto = cloneInto
  137. Compat.exportFunction = exportFunction
  138. } else {
  139. Compat.cloneInto = Compat.exportFunction = value => value
  140. Compat.unsafeWindow = unsafeWindow
  141. }
  142.  
  143. console.debug('hooking XHR#send:', Compat.unsafeWindow.XMLHttpRequest.prototype.send)
  144.  
  145. /*
  146. * replace the default XHR#send with our custom version, which scans responses
  147. * for tweets and expands their URLs
  148. */
  149.  
  150. Compat.unsafeWindow.XMLHttpRequest.prototype.send = Compat.exportFunction(
  151. hookXHRSend(window.XMLHttpRequest.prototype.send),
  152. Compat.unsafeWindow
  153. )