block CCP propaganda tweets likers

Block with love.

目前為 2023-08-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name        block CCP propaganda tweets likers
// @namespace   https://eolstudy.com
// @version     4.3.5
// @description Block with love.
// @author      amormaid
// @run-at      document-end
// @grant       GM_registerMenuCommand
// @match       https://twitter.com/*
// @match       https://mobile.twitter.com/*
// @match       https://tweetdeck.twitter.com/*
// @exclude     https://twitter.com/account/*
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @license     MIT
// ==/UserScript==

/* Inspired by Twitter-Block-With-Love https://greasyfork.org/en/scripts/398540-twitter-block-with-love */

(_ => {

    function stringify(obj, sep, eq) {
        sep = sep || '&';
        eq = eq || '=';
        let str = "";
        for (var k in obj) {
            str += k + eq + decodeURIComponent(obj[k]) + sep
        }
        return str.slice(0, -1)
    };

    function parse(str) {
        var obj = new Object();
        strs = str.split("&");
        for (var i = 0; i < strs.length; i++) {
            let index = strs[i].indexOf("=")
            obj[strs[i].slice(0, index)] = decodeURIComponent(strs[i].slice(index + 1));
        }
        return obj;
    }

    //解析url地址
    function getRequest() {
        var url = location.search; //获取url中"?"符后的字串
        var theRequest = new Object();
        if (url.indexOf("?") != -1) {
            var str = url.substr(1);
            return parse(str)
        }
    }


    const translations = {
      // Please submit a feedback on Greasyfork.com if your language is not in the list bellow
      'en': {
        lang_name: 'English',
        like_title: 'Liked by',
        like_list_identifier: 'Timeline: Liked by',
        retweet_title: 'Retweeted by',
        retweet_list_identifier: 'Timeline: Retweeted by',
        block_btn: 'Block Pinker',
        block_success: 'All users blocked!',
        mute_btn: 'Mute all',
        mute_success: 'All users muted!',
        include_original_tweeter: 'Include the original Tweeter',
        logs: 'Logs',
        list_members: 'List members',
        list_members_identifier: 'Timeline: List members',
        block_retweets_notice: 'TBWL has only blocked users that retweeted without comments.\n Please block users retweeting with comments manually.'
      }
    }
    let i18n = translations.en

    function get_theme_color (){
      const close_icon = $('div[aria-label] > div[dir="auto"] > svg[viewBox="0 0 24 24"]')[0]
      return window.getComputedStyle(close_icon).color
    }

    async function sleep (ms = 50) {
      return new Promise(resolve => setTimeout(resolve, ms))
    }

    function component_to_hex (c) {
      if (typeof(c) === 'string') c = Number(c)
      const hex = c.toString(16);
      return hex.length === 1 ? ("0" + hex) : hex;
    }

    function rgb_to_hex (r, g, b) {
      return "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b);
    }

    function get_cookie (cname) {
      let name = cname + '='
      let ca = document.cookie.split(';')
      for (let i = 0; i < ca.length; ++i) {
        let c = ca[i].trim()
        if (c.indexOf(name) === 0) {
          return c.substring(name.length, c.length)
        }
      }
      return ''
    }

    function getStorage (name) {
      try {
        return window.JSON.parse(sessionStorage.getItem(name) || '[]')
      } catch (err) {
        sessionStorage.setItem(name, '[]')
        return []
      }
    }

    function setStorage (name, val) {
      sessionStorage.setItem(name, window.JSON.stringify(val))
    }

    function get_ancestor (dom, level) {
      for (let i = 0; i < level; ++i) {
        dom = dom.parent()
      }
      return dom
    }

    const ajax = axios.create({
      baseURL: 'https://api.twitter.com',
      withCredentials: true,
      headers: {
        // 'User-Agent': '',
        'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
        'X-Twitter-Auth-Type': 'OAuth2Session',
        'X-Twitter-Active-User': 'yes',
        'X-Csrf-Token': get_cookie('ct0')
      }
    })

    // https://developer.twitter.com/en/docs/twitter-api/v1
    // users this user is following
    async function get_friends (userId) {
      const cachedFriends = getStorage('friends')
      if (cachedFriends && cachedFriends.length) {
        return cachedFriends
      }
      const my_id = get_cookie('twid').replace('u%3D', '')
      const users = await ajax.get(`/1.1/friends/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
        res => res.data.ids
      ) || []
      // console.log('get_friends', users)
      setStorage('friends', window.JSON.stringify(users))
      return users
    }

    // users follow this user
    async function get_followers (userId) {
      const cachedUsers = getStorage('followers')
      if (cachedUsers && cachedUsers.length) {
        return cachedUsers
      }
      const my_id = get_cookie('twid').replace('u%3D', '')
      const users = await ajax.get(`/1.1/followers/ids.json?user_id=${userId || my_id}&count=5000&stringify_ids=true`).then(
        res => res.data.ids
      ) || []
      // console.log('get_followers', users)
      setStorage('followers', window.JSON.stringify(users))
      return users
    }

    async function get_list_menber (listId) {
      const cachedUsers = getStorage('ccpmember')
      if (cachedUsers && cachedUsers.length) {
        return cachedUsers
      }
      const users = await ajax.get(`/1.1/lists/members.json?list_id=${listId}&count=5000`).then(
        res => res.data.users
      )
      // console.log('get_list_menber', users)
      const newUsers =  (users || []).map(({ id_str }) => id_str)
      setStorage('ccpmember', window.JSON.stringify(newUsers))
      return newUsers
    }

    // https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/create-manage-lists/overview
    async function get_list_tweets(listId, tweets_number = 100, last_id) {
      const tweets_list_res = await ajax.get(`/1.1/lists/statuses.json?list_id=${listId}&include_rts=false&count=${tweets_number}${last_id ? `&max_id=${last_id}` : ''}`)
      // console.log('get_list_tweets res before ', last_id, ' is ',  tweets_list_res.data)
      return (tweets_list_res || {}).data || []
    }

    function get_tweet_id () {
      // https://twitter.com/any/thing/status/1234567/anything => 1234567/anything => 1234567
      return location.href.split('status/')[1].split('/')[0]
    }

    // function get_list_id () {
    //   // https://twitter.com/any/thing/lists/1234567/anything => 1234567/anything => 1234567
    //   return location.href.split('lists/')[1].split('/')[0]
    // }

    // fetch_likers and fetch_no_comment_retweeters need to be merged into one function
    async function fetch_likers (tweetId) {
      const users = await ajax.get(`/2/timeline/liked_by.json?tweet_id=${tweetId}`).then(
        res => res.data.globalObjects.users
      )

      let likers = []
      Object.keys(users).forEach(user => likers.push(user)) // keys of users are id strings
      return likers
    }

    // async function fetch_no_comment_retweeters (tweetId) {
    //   const users = (await ajax.get(`/2/timeline/retweeted_by.json?tweet_id=${tweetId}`)).data.globalObjects.users

    //   let targets = []
    //   Object.keys(users).forEach(user => targets.push(user))
    //   return targets
    // }

    // async function fetch_list_members (listId) {
    //   const users = (await ajax.get(`/1.1/lists/members.json?list_id=${listId}`)).data.users
    //   let members = []
    //   members = users.map(u => u.id_str)
    //   return members
    // }

    function block_user (id) {
      // ajax.post('/1.1/blocks/create.json', Qs.stringify({
      ajax.post('/1.1/blocks/create.json', stringify({
        user_id: id
      }), {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      })
    }

  //   function mute_user (id) {
  //     ajax.post('/1.1/mutes/users/create.json', Qs.stringify({
  //       user_id: id
  //     }), {
  //       headers: {
  //         'Content-Type': 'application/x-www-form-urlencoded'
  //       }
  //     })
  //   }


    async function get_list_likers_a (tweets_list = [], pre_id, tweets_number_max = 5,count = 0) {
      const max_tweets_per_request = 2
      // let tweets_list = []
      let last_id = pre_id

      if (tweets_list.length >= tweets_number_max) {
        return tweets_list
      }
      if (count >= 5) {
        return tweets_list
      }

      let new_list
      new_list = await get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
      // it is so wired, code has TDZ con not access new_list before initialize
      // console.log('pre_id is ', pre_id, new_list)
      // console.log('new_list', new_list)
      (new_list || []).forEach(list_item => {
        const {id} = list_item || {}
        last_id = id
        tweets_list.push(list_item)
      })
      return await get_list_likers(tweets_list, last_id, tweets_number_max, count + 1)
    }


    async function get_list_likers (tweets_number_max = 1000,) {
      const max_tweets_per_request = Math.min(85, tweets_number_max)
      let tweets_list = []
      let last_id

      const query_times = Math.ceil(tweets_number_max/max_tweets_per_request)
      const promise_sequence = Array.from(new Array(query_times + 1)).reduce(async (acc, cur) => {
        // const pre_res = await acc || []
        const [, pre_res = []] =  await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
        const last_tweet = (pre_res.slice(-1) || [])[0] || {}
        const {id} = last_tweet
        // console.log('pre_res ', pre_res)
        last_id = id
        tweets_list = [...tweets_list, ...pre_res]

        if (tweets_list.length >= tweets_number_max) {
          return  Promise.resolve()
        } else {
          const new_request = get_list_tweets('1661581723006803968', max_tweets_per_request, last_id)
          return new_request
        }
      }, Promise.resolve())
      await promise_sequence
      console.log('get_list_likers end')
      return tweets_list
    }

    let block_status
    // block_all_liker and block_no_comment_retweeters need to be merged
    async function block_all_pinker () {
      if (block_status) {
        return success_notice('', 'API limit, please wait')
      }
      block_status = 'running'
      //   const tweetId = get_tweet_id()
      const [my_followers, my_friends, listMember] = await Promise.all([
        get_followers(),
        get_friends(),
        get_list_menber('1497432788634841089') // ccp propaganda list
      ])

      // console.log('my_followers', my_followers)
      // console.log('my_friends', my_friends)
      // console.log('listMember', listMember)

      // https://twitter.com/harry_shosta/status/1669140306481278977/likes

      // const likers = await fetch_likers(tweetId)
      // console.log('likers', likers)

      // console.log('newLikers', newLikers)
      // console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
      // likers.forEach(id => block_user(id))
      // success_notice(i18n.like_list_identifier, i18n.block_success)
      try {
        let likers = []

        if (window.location.href.match(/\d+\/likes$/)) {
          const tweetId = get_tweet_id()
          likers = await fetch_likers(tweetId)
          console.log('likers', likers)
  
          console.log('newLikers', newLikers)
          console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
          // likers.forEach(id => block_user(id))
          // success_notice(i18n.like_list_identifier, i18n.block_success)
        }


        if (!window.location.href.match(/\d+\/likes$/)) {
          const block_like_threshlod = 99
          const query_tweets_number = 100
          const twetts_aa = await get_list_likers(query_tweets_number)
          console.log('twetts_aa  ', twetts_aa)
          // twetts_aa.forEach(async (item, index) => {
          //   index < 1 && console.log('twetts_aa item ', item)
          //   const {id_str, favorite_count} = item
          //   if (favorite_count > 99) {
          //     const _likers = await fetch_likers(id_str)
          //     _likers.forEach(likerId => likers.push(likerId))
          //   }
    
          // })
          const promise_sequence = ([...twetts_aa, {}]).reduce(async (acc, cur) => {
            const pre_res = await acc || []
            likers = [...likers, ...pre_res]
            // const last_tweet = (pre_res.slice(-1) || [])[0] || {}
            const {id_str, favorite_count} = cur
            if (favorite_count > block_like_threshlod && id_str) {
              const new_request = await fetch_likers(id_str)
              // console.log(cur, new_request, '\r\n\r\n\r\n')
              return new_request
            } else {
              return  Promise.resolve()
            }
          }, Promise.resolve())
          await promise_sequence
        }

  
        const newLikers = likers.filter(id => {
          const flag_a = !my_followers.includes(id)
          const flag_b = !my_friends.includes(id)
          const flag_c = !listMember.includes(id)
          return flag_a && flag_b && flag_c
        })
  
        const pure_new_likers = Array.from(new Set(newLikers || []))
  
        console.log('pure_new_likers ', pure_new_likers)
        const block_sequence = ([...pure_new_likers, '']).reduce(async (acc, cur, index) => {
          await Promise.all([sleep(Math.ceil(Math.random() * 100)), acc])
          if (index > 0 && index % 250 === 0) {
            // limit 300 per minute
            await sleep(15 * 60 * 1000)
          }
          let id = cur
          if (id) {
            block_notice(i18n.like_list_identifier, index, pure_new_likers.length)
            const new_request = block_user(id)
            return new_request
          } else {
            return  Promise.resolve()
          }
        }, Promise.resolve())
        await block_sequence
        // likers.forEach(id => block_user(id))
        // success_notice(i18n.like_list_identifier, i18n.block_success)
        block_status = null
      } catch (err) {
        console.error(err)
        block_status = null
        success_notice('', 'block failed')
      }


    }


    function success_notice (identifier, success_msg) {

      const btnNode = document.createElement('div')
      btnNode.innerText = success_msg
      btnNode.style.cssText = `
        position: absolute;
        left: calc(50% - 25vw);
        top: 40vw;
        width: 50vw;
        height: 10vw;
        background-color: rgba(255, 255, 255, 0.5);
        border: 1px solid #eaeaea;
        text-align: center;
        line-height: 10vw;
      `
      document.body.append(btnNode)
      setTimeout( () => {
        btnNode.parentNode.removeChild(btnNode)
      }, 3000)
    }

    let timer_id
    function block_notice (identifier, current, total) {
      let btnNode = document.getElementById('block_root')
      if (!btnNode) {
        btnNode = document.createElement('div')
        btnNode.setAttribute('id', 'block_root')
      }

      btnNode.innerText = `bocked ${current + 1} / ${total}`
      btnNode.style.cssText = `
        position: absolute;
        left: calc(50% - 25vw);
        top: 40vw;
        width: 50vw;
        height: 10vw;
        background-color: rgba(255, 255, 255, 0.5);
        border: 1px solid #eaeaea;
        text-align: center;
        line-height: 10vw;
      `
      document.body.append(btnNode)
      clearTimeout(timer_id)
      timer_id = setTimeout( () => {
        btnNode.parentNode.removeChild(btnNode)
      }, 5000)
    }

    function mount_button (parentDom, name, executer) {
      const btnNode = document.createElement('button')
      btnNode.innerText = name
      btnNode.style.cssText = `
        position: absolute;
        right: 0px;
        top: 5px;
      `
      btnNode.addEventListener('click', executer)
      parentDom.append(btnNode)
    }


    function use_MutationObserver () {

      const targetNode = document.getElementById('react-root');
      // Options for the observer (which mutations to observe)
      const config = { attributes: true, childList: true, subtree: true };
      // Callback function to execute when mutations are observed
      const callback = (mutationList, observer) => {
        for (const mutation of mutationList) {

          if (mutation.type === 'childList' && mutation.target?.innerText?.includes(i18n.like_title)) {
            // console.log('mutation', mutation)
            const domList = Array.from(mutation.target.getElementsByTagName('h2'))
            const domTarget = domList.find(i => i.innerText === i18n.like_title)
            if (domTarget) {
              mount_button(domTarget, i18n.block_btn, block_all_pinker)
            }
          }
          // if (mutation.type === 'childList') {
          //   console.log('A child node has been added or removed.');
          // } else if (mutation.type === 'attributes') {
          //   console.log(`The ${mutation.attributeName} attribute was modified.`);
          // }
        }
      };
      const observer = new MutationObserver(callback)
      observer.observe(targetNode, config);
    }

    const mount_root = document.getElementsByTagName('body')[0]
    mount_button(mount_root, i18n.block_btn, block_all_pinker)
    // main()
  })()