Twitter Prime

Free yourself from X ads and analytics.

Устаревшая версия за 06.09.2023. Перейдите к последней версии.

// ==UserScript==
// @name        Twitter Prime
// @description Free yourself from X ads and analytics.
// @namespace   Itsnotlupus Industries
// @author      itsnotlupus
// @license     MIT
// @version     1.1
// @match       https://twitter.com/*
// @match       https://platform.twitter.com/*
// @run-at      document-start
// @grant       none
// @require     https://greasyfork.org/scripts/468394-itsnotlupus-tiny-utilities/code/utils.js
// @require     https://greasyfork.org/scripts/472943-itsnotlupus-middleman/code/middleman.js
// @require     https://greasyfork.org/scripts/473998-itsnotlupus-react-tools/code/react-tools.js
// ==/UserScript==
/* jshint esversion:11 */
/* global globalThis, ReactTools, middleMan, decodeEntities, logGroup */

// NOTE: You can edit the following config flags to taste.
const CONFIG = {
  // This setting hides the "Verified" tab and "Get Verified" upsell messages.
  hide_upselling: true,
  // When set to true, this setting will show a "Community Notes" tab.
  show_community_notes_tag: false,
  // When set to true, this settings will show a "Spaces" tab to find Spaces to listen to.
  // The UX is a bit wonky on desktop, which might be why it's hidden. Still, it's neat.
  show_spaces_tab: false
};

// disable google analytics
(globalThis.unsafeWindow??window).ga = (method, field, details) => {};

// hook into Twitter's React tree, and find a redux store off of one of the components there.
async function withReduxState(fn) {
  const react = new ReactTools();
  const disconnect = react.observe(() => {
    const store = react.getProp('store');
    if (store) {
      fn(store.getState());
      disconnect();
    }
  });
}

withReduxState(state => {
  // we're mutating a redux store. this is widely seen as poor form, as it skips/breaks most of redux' logic.
  applyConfigOverrides(state.featureSwitch.user);
});

function applyConfigOverrides(json) {
  Object.assign(json.config, {
    subscriptions_sign_up_enabled: { value: !CONFIG.hide_upselling },
    responsive_web_birdwatch_note_writing_enabled: { value: CONFIG.show_community_notes_tag },
    voice_rooms_discovery_page_enabled: { value: CONFIG.show_spaces_tab },
  });
  return json;
}

function unshortenLinks(obj) {
  const map = {};
  // 1st pass: gather associations between t.co and actual URLs
  (function populateURLMap(obj) {
    if (obj.url && obj.expanded_url) map[obj.url] = obj.expanded_url;
    Object.keys(obj).forEach(k => obj[k] && typeof obj[k] == "object" && populateURLMap(obj[k]));
  })(obj);
  // 2d pass: replace (almost) any string that contains a t.co string
  (function replaceURLs(obj) {
    Object.keys(obj).forEach(key => ({
      string() { if (map[obj[key]] && key!=='full_text') obj[key] = map[obj[key]]; },
      object() { if (obj[key] != null) replaceURLs(obj[key]); }
    }[typeof obj[key]]?.()));
  })(obj);
  return obj;
}

// Why struggle to remove ads/promoted tweets from X's html tag soup when you can simply remove them from the wire?
function removeAds(obj) {
  if (obj && typeof obj == 'object') {
    Object.keys(obj).forEach(key => {
      if (obj[key]?.content?.itemContent?.promotedMetadata ||
          obj[key]?.item?.itemContent?.promotedMetadata) {
        const { itemContent } = obj[key].content ?? obj[key].item;
        const { name, screen_name } = itemContent.promotedMetadata.advertiser_results?.result?.legacy ?? {};
        const { result = {} } = itemContent.tweet_results;
        const { full_text, id_str } = result.legacy ?? result.tweet?.legacy ?? {};
        const url = `https://twitter.com/${screen_name}/status/${id_str}`;
        logGroup(`[AD REMOVED] @${screen_name} ${url}`, `From ${name}`, decodeEntities(full_text));
        delete obj[key];
      } else {
        removeAds(obj[key]);
      }
    });
  }
  return obj;
}

const transformJSON = transform => async (req,res,err) => {
  if (err) return;

  return Response.json(transform(await res.json()), {
    status: res.status,
    headers: res.headers
  });
};

const processSettings = transformJSON(applyConfigOverrides);
const processTwitterJSON = transformJSON(json => removeAds(unshortenLinks(json)));

// Twitter doesn't *need* to know what's happening in your browser. They'd like to, but maybe you have a say too.
// The next line means "If you see a network request like this, short-circuit it and return an empty response instead."
middleMan.addHook("https://twitter.com/i/api/1.1/jot/*", { requestHandler: () => new Response() });

// Intercept requests that would invalidate our config flags
middleMan.addHook("https://api.twitter.com/1.1/help/settings.json?*", { responseHandler: processSettings });

// Intercept twitter API calls to use real URLs and remove ads.
middleMan.addHook("https://twitter.com/i/api/graphql/*", { responseHandler: processTwitterJSON });
middleMan.addHook("https://twitter.com/i/api/*.json?*", { responseHandler: processTwitterJSON });
middleMan.addHook("https://cdn.syndication.twimg.com/tweet-result?*", { responseHandler: processTwitterJSON });