block CCP propaganda tweets likers

Block with love.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        block CCP propaganda tweets likers
// @namespace   https://eolstudy.com
// @version     4.5.7
// @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/*

// @license     MIT
// ==/UserScript==

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

(_ => {
    // @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js

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

      btnNode.innerText = message
      btnNode.style.cssText = `
        position: fixed;
        left: calc(50% - 100px);
        top: calc(50vh - 60px);;
        width: 200px;
        height: 120px;
        background-color: rgba(255, 255, 255, 0.5);
        border: 1px solid #eaeaea;
        text-align: center;
        line-height: 120px;
        font-size: 16px;
      `
      document.body.append(btnNode)
      clearTimeout(timer_id)
      timer_id = setTimeout( () => {
        btnNode.parentNode.removeChild(btnNode)
      }, 5000)
    }


    class HTTPError extends Error {
        constructor(response, ...params) {
          // Pass remaining arguments (including vendor specific ones) to parent constructor
          super(...params);

          this.response = response;

          // Maintains proper stack trace for where our error was thrown (only available on V8)
          if (Error.captureStackTrace) {
            Error.captureStackTrace(this, HTTPError);
          }

          this.name = 'HTTPError';
        }
    }

    // window.axios = {};
    const ajax = {};
    ['get','post','put','delete'].forEach(requestType => {
      ajax[requestType] = function (url, data){
        return fetch(`https://api.twitter.com${url}`, {
            headers: {
                // modify Content-Type for /1.1/blocks/create.json
                "Content-Type": requestType.toUpperCase() === 'POST' ? 'application/x-www-form-urlencoded' : "application/json",
                "Accept": "application/json",
                //"X-Requested-With": "XMLHttpRequest",
                // "X-CSRF-Token": token.content
                'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
                'X-Twitter-Auth-Type': 'OAuth2Session',
                'X-Twitter-Active-User': 'yes',
                'X-Csrf-Token': get_cookie('ct0')

            },
            method: requestType,
            mode: "cors",
            credentials: "include",
            body: requestType.toUpperCase() === 'POST' ? stringify(data) : JSON.stringify(data),
        })
          .then( async response=>{
              if (!response.ok) {
                  let data=await response.json();
                  response.data = data;
                  console.log(response);
                  throw new HTTPError(response, response.statusText);
              }
              return response;
          }).then(response=>{
              return response.json();
          }).then(jsonResponse => {
              return {data:jsonResponse};
          });
      }
    });


    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;
        var theRequest = new Object();
        if (url.indexOf("?") != -1) {
            var str = url.substr(1);
            return parse(str)
        }
    }

    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
    async function get_user_timeline (screen_name) {
      // https://api.twitter.com/1.1/statuses/user_timeline.json
      const tweets_list = await ajax.get(`/1.1/statuses/user_timeline.json?screen_name=${screen_name}&count=5000&stringify_ids=true`).then(
        res => res.data
      ) || []
      return tweets_list
    }
    // 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'
      //   }
      // })
      ajax.post('/1.1/blocks/create.json', {
        user_id: id
      })
    }

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

    let last_id
    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 last_run
    let block_list_cache = []
    const block_like_threshlod = 50 // 99
    const block_number_per_batch = 60
    const query_tweets_number = 120 // 100

    // block_all_liker and block_no_comment_retweeters need to be merged
    async function block_all_pinker () {
      if (last_run && (new Date().getTime() - last_run < 30 * 1000)) {
        return block_notice('API limit, please wait')
      }
      block_notice('blocker running !')
      last_run = new Date().getTime()

      try {
        let likers = []

        // at likes page, block likes user
        if (window.location.href.match(/\d+\/likes$/)) {
          const tweetId = get_tweet_id()
          likers = await fetch_likers(tweetId)
        }



        if (!window.location.href.match(/\d+\/likes$/)) {
          let twetts_asemble = []
          // at home page, block list tweets likes user
          if (window.location.pathname.includes('lists/')) {
            // at list menber page, block member tweets likes user
            twetts_asemble = await get_user_timeline(window.location.pathname.replace('/', ''))
            console.log('get twetts_asemble ', twetts_asemble.length)
          } else {
            return
          }

          likers = await get_likers_by_tweet_list(twetts_asemble)

        }

        await block_action(likers)

        // likers.forEach(id => block_user(id))
        // last_run = null


      } catch (err) {
        console.error(err)
        // last_run = null
        block_notice('block failed')
      }
    }

    async function block_all_pinker_home () {
      last_run = last_run || (localStorage.getItem('last_run') - 0)
      if (last_run && (new Date().getTime() - last_run < 20 * 60 * 1000)) {
        return 
        // block_notice('API limit, please wait')
      }
      // block_notice('blocker running !')
      last_run = new Date().getTime()
      //   const tweetId = get_tweet_id()
      try {
        // at home page, block list tweets likes user

        const twetts_asemble = await get_list_likers(query_tweets_number)
        console.log('twetts_asemble  ', twetts_asemble.length)
        const likers = await get_likers_by_tweet_list(twetts_asemble)

        const pure_new_likers = await block_action(Array.from(new Set([...block_list_cache, ...likers])))

        block_list_cache = pure_new_likers.slice(block_number_per_batch)
        console.log('block_list_cache has ', block_list_cache.length, ' users')
        console.log(new Date().toLocaleString() + '\r\n\r\n')
        setTimeout(block_all_pinker_home, 20 * 60 * 1000)
        localStorage.setItem('last_run', new Date().getTime())
      } catch (err) {
        console.error(err)
        // last_run = null
        block_notice('block failed')
      } finally {

      }


    }

    async function get_likers_by_tweet_list(twetts_asemble) {
      let likers = []
      const promise_sequence = ([...twetts_asemble, {}]).reduce(async (acc, cur, index) => {
        const pre_res = await acc || []
        likers = [...likers, ...pre_res]
        // const last_tweet = (pre_res.slice(-1) || [])[0] || {}
        const {id_str, favorite_count} = cur
        // only query 100th ~ 200th tweet liker
        if (favorite_count > block_like_threshlod && id_str && index > 100) {
          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
      console.log('find ', likers.length, ' pinkers in ', twetts_asemble.length, ' tweets')
      return likers
    }

    async function block_action(likers) {

      const [my_followers, my_friends, listMember] = await Promise.all([
        get_followers(),
        get_friends(),
        get_list_menber('1497432788634841089') // ccp propaganda list
      ])

      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 === 80 || index === 350 || index === 700) {
        //   // limit 300 per minute
        //   await sleep(15 * 60 * 1000)
        // }
        let id = cur
        if (id && index < block_number_per_batch) {
          block_notice(`bocked ${index + 1} / ${pure_new_likers.length}`)
          const new_request = block_user(id)
          return new_request
        } else {
          return  Promise.resolve()
        }
      }, Promise.resolve())
      await block_sequence
      return pure_new_likers
    }



    function mount_button (parentDom, name, executer) {
      const btnNode = document.createElement('button')
      btnNode.innerText = name
      btnNode.style.cssText = `
        position: fixed;
        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('liked by')) {
    //         // console.log('mutation', mutation)
    //         const domList = Array.from(mutation.target.getElementsByTagName('h2'))
    //         const domTarget = domList.find(i => i.innerText === 'liked by')
    //         if (domTarget) {
    //           mount_button(domTarget, 'Block Pinker', 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);
    // }

    if (window.top === window) {
      const mount_root = document.getElementsByTagName('body')[0]
      mount_button(mount_root, 'Block Pinker', block_all_pinker)
      setTimeout(block_all_pinker_home, 10 * 1000)
    }

    // setTimeout(block_all_pinker_home, 5 * 1000)
    // use_MutationObserver()
    // main()
  })()