Fck X (Twitter)

F**k X!

// ==UserScript==
// @name         Fck X (Twitter)
// @license      MIT
// @namespace    zam157.fck-x
// @version      0.2
// @run-at       document-start
// @description  F**k X!
// @author       Zam157
// @homepageURL  https://github.com/zam157/tamper/tree/master/scripts/fck-x
// @supportURL   https://github.com/zam157/tamper/tree/master/scripts/fck-x
// @match        https://twitter.com/**
// @match        https://x.com/**
// ==/UserScript==

(() => {
  // #region Filter promoted tweets
  function filterPromotedInstructions(instructions) {
    return instructions.map((instruction) => {
      if (instruction.type !== 'TimelineAddEntries')
        return instruction

      // instruction.entries = instruction.entries.filter(entry => !entry.entryId.includes('promoted-tweet'))
      instruction.entries = instruction.entries.filter((entry) => {
        if (entry.content?.itemContent?.promotedMetadata)
          return false
        if (entry.content?.items?.[0]?.item?.itemContent?.promotedMetadata)
          return false
        return true
      })
      return instruction
    })
  }
  /**
   * @param {string} json
   * @param {'timeline'|'detail'} type
   */
  function rewriteResponse(json, type) {
    const obj = JSON.parse(json)
    const instructions = (() => {
      if (type === 'timeline')
        return obj.data?.home?.home_timeline_urt?.instructions
      if (type === 'detail')
        return obj.data?.threaded_conversation_with_injections_v2?.instructions
      return null
    })()
    if (!instructions)
      return json
    const _instructions = filterPromotedInstructions(instructions)
    if (type === 'timeline')
      obj.data.home.home_timeline_urt.instructions = _instructions
    if (type === 'detail')
      obj.data.threaded_conversation_with_injections_v2.instructions = _instructions
    return JSON.stringify(obj)
  }
  // #endregion

  function inject(target, fnName, cb) {
    const originalFn = target[fnName]
    target[fnName] = function (...args) {
      cb.call(this, originalFn.bind(this), args)
    }
  }

  inject(XMLHttpRequest.prototype, 'open', function (originalFn, args) {
    const [, url] = args
    let type = null
    if (/HomeTimeline|HomeLatestTimeline/.test(url))
      type = 'timeline'
    else if (/\/api\/graphql\/.+\/TweetDetail/.test(url))
      type = 'detail'
    if (type) {
      const getter = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, 'response').get
      for (const propertyName of ['response', 'responseText']) {
        Object.defineProperty(this, propertyName, {
          get: () => {
            const result = getter.call(this)
            if (!result)
              return

            return rewriteResponse(result, type)
          },
        })
      }
    }

    return originalFn(...args)
  })
})()