Twitter Prime

Free yourself from X ads and analytics.

Pada tanggal 12 Desember 2023. Lihat %(latest_version_link).

// ==UserScript==
// @name        Twitter Prime
// @description Free yourself from X ads and analytics.
// @namespace   Itsnotlupus Industries
// @author      itsnotlupus
// @license     MIT
// @version     1.3
// @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?version=1247001
// @require     https://greasyfork.org/scripts/472943-itsnotlupus-middleman/code/middleman.js?version=1239323
// @require     https://greasyfork.org/scripts/473998-itsnotlupus-react-tools/code/react-tools.js?version=1246974
// ==/UserScript==
/* jshint esversion:11 */
/* eslint-env es2020 */
/* global ReactTools, middleMan, traverse, decodeEntities, logGroup */

// NOTE: You can edit the following config flags to taste.
const CONFIG = {
  // This setting hides upsell, subscriptions, grok and super follow nonsense.
  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
};

// Apply our config settings to a Twitter user config object.
function applyConfigOverrides(obj) {
  // disable any config key that has a whiff of monetization
  Object.keys(obj.config).forEach(key => key.match(/_upsell|subscriptions_|super_follow/) && (obj.config[key] = { value: !CONFIG.hide_upselling }));
  // other optional fun config tweaks
  return Object.assign(obj.config, {
    responsive_web_birdwatch_note_writing_enabled: { value: CONFIG.show_community_notes_tag },
    voice_rooms_discovery_page_enabled: { value: CONFIG.show_spaces_tab },
  });
}

// Replace t.co shortened links with real URLs
function unshortenLinks(obj) {
  const map = {};
  // 1st pass: gather associations between t.co and actual URLs
  traverse(obj, (obj) => {
    if (obj && obj.url && obj.expanded_url) map[obj.url] = obj.expanded_url;
  });
  // 2d pass: replace (almost) any string that contains a t.co URL
  traverse(obj, (str, parent, key) => {
    if (typeof str == 'string' && map[str] && key !== 'full_text') parent[key] = map[str];
  });
  return obj;
}

// Log removed ads to the console, for the curious cats among us.
function logAd(obj) {
  const { itemContent } = obj.content ?? obj.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));
}

// 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) {
  traverse(obj, (obj, parent, key) => {
    if (obj && (obj.content ?? obj.item)?.itemContent?.promotedMetadata) {
      logAd(obj);
      delete parent[key];
      return false;
    }
  });
  return obj;
}

// Middleman response handler generator. Just add a json editing function.
function transformResponse(transform) {
  return async (req, res, err) => {
    if (err) return;

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

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

  // Mutate Twitter's redux store. This is widely seen as poor form, as it skips/breaks most of redux' logic.
  ReactTools.withReduxState(state => applyConfigOverrides(state.featureSwitch.user));

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

  // Intercept twitter API calls to use real URLs and remove ads.
  const processTwitterJSON = transformResponse(obj => removeAds(unshortenLinks(obj)));
  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 });

  // 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() });
}

main();