Block Promoted Tweets and Stuff

If twitter promotes their tweet, we'll block it, or your money back!

  1. // ==UserScript==
  2. // @name Block Promoted Tweets and Stuff
  3. // @name:fr Bloque les Gazouillis Sponsorisés et Tout Ça
  4. // @namespace Itsnotlupus Industries
  5. // @match https://twitter.com/*
  6. // @version 2.0
  7. // @author Itsnotlupus
  8. // @description If twitter promotes their tweet, we'll block it, or your money back!
  9. // @description:fr Si twitter promouvois leur gazouillage, on les bloque, ou remboursé!
  10. // @license MIT
  11. // @match https://twitter.com/*
  12. // @match https://platform.twitter.com/*
  13. // @run-at document-start
  14. // @grant none
  15. // @require https://greatest.deepsurf.us/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js
  16. // @require https://greatest.deepsurf.us/scripts/472943-itsnotlupus-middleman/code/middleman.js
  17. // @require https://greatest.deepsurf.us/scripts/473998-itsnotlupus-react-tools/code/react-tools.js
  18. // ==/UserScript==
  19.  
  20. log("This script is deprecated. Please switch to https://greatest.deepsurf.us/en/scripts/474045-twitter-prime to continue getting updates.");
  21. // Since the script largely stopped working after Twitter updated their markup, the script below is actually from "Twitter Prime" linked above.
  22. // This means things should continue to "just work", even for people that don't pay attention to their dev tools console logs.
  23.  
  24. /* jshint esversion:11 */
  25. /* global globalThis, ReactTools, middleMan, decodeEntities, logGroup */
  26.  
  27. // disable google analytics
  28. (globalThis.unsafeWindow??window).ga = (method, field, details) => {};
  29.  
  30. // hook into Twitter's React tree, and find a redux store off of one of the components there.
  31. async function withReduxState(fn) {
  32. const react = new ReactTools();
  33. const disconnect = react.observe(() => {
  34. const store = react.getProp('store');
  35. if (store) {
  36. fn(store.getState());
  37. disconnect();
  38. }
  39. });
  40. }
  41.  
  42. withReduxState(state => {
  43. // we're mutating a redux store. this is widely seen as poor form, as it skips/breaks most of redux' logic.
  44. Object.assign(state.featureSwitch.user.config, {
  45. //// The next line prevents the appearance of "get verified" upsell messaging.
  46. subscriptions_sign_up_enabled: { value: false },
  47. //// If you uncomment it, the next line unlocks the "Community Notes" UI, giving you some visibility into that process.
  48. // responsive_web_birdwatch_note_writing_enabled: { value: true },
  49. //// If you uncomment it, the line below unlocks a way to find live and upcoming Spaces.
  50. //// The UX is a bit wonky on desktop, which might be why it's hidden. Still, it's neat.
  51. // voice_rooms_discovery_page_enabled: { value: true },
  52. });
  53. });
  54.  
  55. // Twitter doesn't *need* to know what's happening in your browser. They'd like to, but maybe you have a say too.
  56. // The next line means "If you see a network request like this, short-circuit it and return an empty response instead."
  57. middleMan.addHook("https://twitter.com/i/api/1.1/jot/*", { requestHandler: () => new Response() });
  58.  
  59. // Intercept twitter API calls to use real URLs and remove ads.
  60. middleMan.addHook("https://twitter.com/i/api/graphql/*", { responseHandler: processTwitterJSON });
  61. middleMan.addHook("https://twitter.com/i/api/*.json?*", { responseHandler: processTwitterJSON });
  62. middleMan.addHook("https://cdn.syndication.twimg.com/tweet-result?*", { responseHandler: processTwitterJSON });
  63.  
  64. async function processTwitterJSON(req, res, err) {
  65.  
  66. function unshortenLinks(obj) {
  67. const map = {};
  68. // 1st pass: gather associations between t.co and actual URLs
  69. (function populateURLMap(obj) {
  70. if (obj.url && obj.expanded_url) map[obj.url] = obj.expanded_url;
  71. Object.keys(obj).forEach(k => obj[k] && typeof obj[k] == "object" && populateURLMap(obj[k]));
  72. })(obj);
  73. // 2d pass: replace (almost) any string that contains a t.co string
  74. (function replaceURLs(obj) {
  75. Object.keys(obj).forEach(key => ({
  76. string() { if (map[obj[key]] && key!=='full_text') obj[key] = map[obj[key]]; },
  77. object() { if (obj[key] != null) replaceURLs(obj[key]); }
  78. }[typeof obj[key]]?.()));
  79. })(obj);
  80. return obj;
  81. }
  82.  
  83. // Why struggle to remove ads/promoted tweets from X's html tag soup when you can simply remove them from the wire?
  84. function removeAds(obj) {
  85. if (obj && typeof obj == 'object') {
  86. Object.keys(obj).forEach(key => {
  87. if (obj[key]?.content?.itemContent?.promotedMetadata ||
  88. obj[key]?.item?.itemContent?.promotedMetadata) {
  89. const { itemContent } = obj[key].content ?? obj[key].item;
  90. const { name, screen_name } = itemContent.promotedMetadata.advertiser_results?.result?.legacy ?? {};
  91. const { result = {} } = itemContent.tweet_results;
  92. const { full_text, id_str } = result.legacy ?? result.tweet?.legacy ?? {};
  93. const url = `https://twitter.com/${screen_name}/status/${id_str}`;
  94. logGroup(`[AD REMOVED] @${screen_name} ${url}`, `From ${name}`, decodeEntities(full_text));
  95. delete obj[key];
  96. } else {
  97. removeAds(obj[key]);
  98. }
  99. });
  100. }
  101. return obj;
  102. }
  103.  
  104. if (err) return;
  105.  
  106. return Response.json(removeAds(unshortenLinks(await res.json())), {
  107. status: res.status,
  108. headers: res.headers
  109. });
  110. }