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. // XXX avoid using the optional-chaining operator for now because GreasyFork
  32. // can't parse it:
  33. //
  34. // const tweets = (data.globalObjects || data.twitter_objects)?.tweets
  35. const objects = data.globalObjects || data.twitter_objects
  36. const tweets = objects ? objects.tweets : objects
  37.  
  38. if (!tweets) {
  39. console.debug("can't find tweets in:", path)
  40. return
  41. }
  42.  
  43. console.info(`scanning links in ${path}:`, data)
  44.  
  45. for (const tweet of Object.values(tweets)) {
  46. const { entities, extended_entities = {} } = tweet
  47. const { urls = [], media = [] } = entities
  48. const { urls: extendedUrls = [], media: extendedMedia = [] } = extended_entities
  49. const locations = [urls, media, extendedUrls, extendedMedia]
  50.  
  51. for (const location of locations) {
  52. for (const item of location) {
  53. item.url = item.expanded_url
  54. }
  55. }
  56. }
  57.  
  58. return data
  59. }
  60.  
  61. /*
  62. * parse and transform the JSON response, handling (catching and reporting) any
  63. * parse or processing errors
  64. */
  65. function transformResponse (json, path) {
  66. let parsed
  67.  
  68. try {
  69. parsed = JSON.parse(json)
  70. } catch (e) {
  71. console.error("Can't parse response:", e)
  72. return
  73. }
  74.  
  75. let transformed
  76.  
  77. try {
  78. transformed = transformLinks(path, parsed)
  79. } catch (e) {
  80. console.error('error transforming JSON:', e)
  81. return
  82. }
  83.  
  84. return transformed
  85. }
  86.  
  87. /*
  88. * replacement for Twitter's default response handler which transforms the
  89. * response if it's a) JSON and b) contains tweet data; otherwise, we leave it
  90. * unchanged
  91. */
  92. function onReadyStateChange (xhr, url) {
  93. const match = url.match(PATTERN)
  94.  
  95. if (!match) {
  96. console.debug("can't match URL:", url)
  97. return
  98. }
  99.  
  100. const path = match[1]
  101. const transformed = transformResponse(xhr.responseText, path)
  102.  
  103. if (transformed) {
  104. const descriptor = { value: JSON.stringify(transformed) }
  105. const clone = Compat.cloneInto(descriptor, Compat.unsafeWindow)
  106. Compat.unsafeWindow.Object.defineProperty(xhr, 'responseText', clone)
  107. }
  108. }
  109.  
  110. /*
  111. * replace the built-in XHR#send method with our custom version which swaps in
  112. * our custom response handler. once done, we delegate to the original handler
  113. * (this.onreadystatechange)
  114. */
  115. function hookXHRSend (oldSend) {
  116. return function send () {
  117. const oldOnReadyStateChange = this.onreadystatechange
  118.  
  119. this.onreadystatechange = function () {
  120. if (this.readyState === 4 && this.responseURL && this.status === 200) {
  121. onReadyStateChange(this, this.responseURL)
  122. }
  123.  
  124. oldOnReadyStateChange.apply(this, arguments)
  125. }
  126.  
  127. return oldSend.apply(this, arguments)
  128. }
  129. }
  130.  
  131. /*
  132. * set up a cross-engine API to shield us from differences between engines so we
  133. * don't have to clutter the code with conditionals.
  134. *
  135. * XXX the functions are only needed for Violentmonkey, and are effectively
  136. * no-ops in other engines
  137. */
  138. if (GM_info.scriptHandler === 'Violentmonkey') {
  139. Compat.unsafeWindow = unsafeWindow.wrappedJSObject
  140. Compat.cloneInto = cloneInto
  141. Compat.exportFunction = exportFunction
  142. } else {
  143. Compat.cloneInto = Compat.exportFunction = value => value
  144. Compat.unsafeWindow = unsafeWindow
  145. }
  146.  
  147. console.debug('hooking XHR#send:', Compat.unsafeWindow.XMLHttpRequest.prototype.send)
  148.  
  149. /*
  150. * replace the default XHR#send with our custom version, which scans responses
  151. * for tweets and expands their URLs
  152. */
  153. Compat.unsafeWindow.XMLHttpRequest.prototype.send = Compat.exportFunction(
  154. hookXHRSend(window.XMLHttpRequest.prototype.send),
  155. Compat.unsafeWindow
  156. )