allen's block

Block with love.

目前為 2022-06-11 提交的版本,檢視 最新版本

// ==UserScript==
// @name        allen's block
// @namespace   https://eolstudy.com
// @version     3.0
// @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
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/qs.min.js
// @require     https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @license     MIT
// ==/UserScript==

/* global axios $ Qs */

(_ => {
  /* Begin of Dependencies */

  // https://gist.githubusercontent.com/BrockA/2625891/raw/9c97aa67ff9c5d56be34a55ad6c18a314e5eb548/waitForKeyElements.js
  /*--- waitForKeyElements():  A utility function, for Greasemonkey scripts,
      that detects and handles AJAXed content.

      Usage example:

          waitForKeyElements (
              "div.comments"
              , commentCallbackFunction
          );

          //--- Page-specific function to do what we want when the node is found.
          function commentCallbackFunction (jNode) {
              jNode.text ("This comment changed by waitForKeyElements().");
          }

      IMPORTANT: This function requires your script to have loaded jQuery.
  */
  function waitForKeyElements (
      selectorTxt,    /* Required: The jQuery selector string that
                          specifies the desired element(s).
                      */
      actionFunction, /* Required: The code to run when elements are
                          found. It is passed a jNode to the matched
                          element.
                      */
      bWaitOnce,      /* Optional: If false, will continue to scan for
                          new elements even after the first match is
                          found.
                      */
      iframeSelector  /* Optional: If set, identifies the iframe to
                          search.
                      */
  ) {
      var targetNodes, btargetsFound;

      if (typeof iframeSelector == "undefined")
          targetNodes     = $(selectorTxt);
      else
          targetNodes     = $(iframeSelector).contents ()
                                            .find (selectorTxt);

      if (targetNodes  &&  targetNodes.length > 0) {
          btargetsFound   = true;
          /*--- Found target node(s).  Go through each and act if they
              are new.
          */
          targetNodes.each ( function () {
              var jThis        = $(this);
              var alreadyFound = jThis.data ('alreadyFound')  ||  false;

              if (!alreadyFound) {
                  //--- Call the payload function.
                  var cancelFound     = actionFunction (jThis);
                  if (cancelFound)
                      btargetsFound   = false;
                  else
                      jThis.data ('alreadyFound', true);
              }
          } );
      }
      else {
          btargetsFound   = false;
      }

      //--- Get the timer-control variable for this selector.
      var controlObj      = waitForKeyElements.controlObj  ||  {};
      var controlKey      = selectorTxt.replace (/[^\w]/g, "_");
      var timeControl     = controlObj [controlKey];

      //--- Now set or clear the timer as appropriate.
      if (btargetsFound  &&  bWaitOnce  &&  timeControl) {
          //--- The only condition where we need to clear the timer.
          clearInterval (timeControl);
          delete controlObj [controlKey]
      }
      else {
          //--- Set a timer, if needed.
          if ( ! timeControl) {
              timeControl = setInterval ( function () {
                      waitForKeyElements (    selectorTxt,
                                              actionFunction,
                                              bWaitOnce,
                                              iframeSelector
                                          );
                  },
                  300
              );
              controlObj [controlKey] = timeControl;
          }
      }
      waitForKeyElements.controlObj   = controlObj;
  }
  /* End of Dependencies */


  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 all',
      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
  }

  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 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: {
      'Authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA',
      'X-Twitter-Auth-Type': 'OAuth2Session',
      'X-Twitter-Active-User': 'yes',
      'X-Csrf-Token': get_cookie('ct0')
    }
  })
  // users this user is following
  async function get_friends (userId) {
    const cachedFriends = window.JSON.parse(sessionStorage.getItem('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)
    sessionStorage.setItem('friends', window.JSON.stringify(users))
    return users
  }
  
  // users follow this user
  async function get_followers (userId) {
    const cachedUsers = window.JSON.parse(sessionStorage.getItem('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)
    sessionStorage.setItem('followers', window.JSON.stringify(users))
    return users
  }

  async function get_list_menber (listId) {
    const cachedUsers = window.JSON.parse(sessionStorage.getItem('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)
    sessionStorage.setItem('ccpmember', window.JSON.stringify(newUsers))
    return newUsers
  }

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


  // block_all_liker and block_no_comment_retweeters need to be merged
  async function block_all_likers () {
    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)
    const likers = await fetch_likers(tweetId)
    console.log('likers', likers)
    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
    })
    console.log('newLikers', newLikers)
    console.log('will not block ', likers.filter(id => !newLikers.includes(id)))
    // likers.forEach(id => block_user(id))
  }


  async function block_no_comment_retweeters () {
    const tweetId = get_tweet_id()
    const retweeters = await fetch_no_comment_retweeters(tweetId)
    retweeters.forEach(id => block_user(id))

    const tabName = location.href.split('retweets/')[1]
    if (tabName === 'with_comments') {
      if (!block_no_comment_retweeters.hasAlerted) {
        block_no_comment_retweeters.hasAlerted = true
        alert(i18n.block_rt_notice)
      }
    }
  }



  async function block_list_members () {
      const listId = get_list_id()
      const members = await fetch_list_members(listId)
      members.forEach(id => block_user(id))
  }


  function success_notice (identifier, success_msg) {
    return _ => {
      const alertColor = 'rgb(224, 36, 94)'
      const container = $('div[aria-label="' + identifier + '"]')
      container.children().fadeOut(400, _ => {
        const notice = $(`
          <div style="
            color: ${alertColor};
            text-align: center;
            margin-top: 3em;
            font-size: x-large;
          ">
            <span>${success_msg}</span>
          </div>
        `)
        container.append(notice)
      })
    }
  }



  function mount_button (parentDom, name, executer, success_notifier) {
    let themeColor = get_theme_color()
    const hoverColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.1)')
    const mousedownColor = themeColor.replace(/rgb/i, "rgba").replace(/\)/, ', 0.2)')
    const btn_mousedown = 'bwl-btn-mousedown'
    const btn_hover = 'bwl-btn-hover'

    $('head').append(`
      <style>
        .bwl-btn-base {
          min-height: 30px;
          padding-left: 1em;
          padding-right: 1em;
          border: 1px solid ${themeColor} !important;
          border-radius: 9999px;
        }
        .${btn_mousedown} {
          background-color: ${mousedownColor};
          cursor: pointer;
        }
        .${btn_hover} {
          background-color: ${hoverColor};
          cursor: pointer;
        }
        .bwl-btn-inner-wrapper {
          font-weight: bold;
          -webkit-box-align: center;
          align-items: center;
          -webkit-box-flex: 1;
          flex-grow: 1;
          color: ${themeColor};
          display: flex;
        }
        .bwl-text-font {
          font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif;
          color: ${themeColor};
        }
      </style>
    `)

    const button = $(`
      <div
        aria-haspopup="true"
        role="button"
        data-focusable="true"
        class="bwl-btn-base"
        style="margin:3px"
      >
        <div class="bwl-btn-inner-wrapper">
          <span>
            <span class="bwl-text-font">${name}</span>
          </span>
        </div>
      </div>
    `)
    .addClass(parentDom.prop('classList')[0])
    .hover(function () {
      $(this).addClass(btn_hover)
    }, function () {
      $(this).removeClass(btn_hover)
      $(this).removeClass(btn_mousedown)
    })
    .on('selectstart', function () {
      return false
    })
    .mousedown(function () {
      $(this).removeClass(btn_hover)
      $(this).addClass(btn_mousedown)
    })
    .mouseup(function () {
      $(this).removeClass(btn_mousedown)
      if ($(this).is(':hover')) {
        $(this).addClass(btn_hover)
      }
    })
    .click(executer)
    .click(success_notifier)

    parentDom.append(button)
  }

  function main () {
    waitForKeyElements('h2:has(> span:contains(' + i18n.like_title + '))', dom => {
      const ancestor = get_ancestor(dom, 3)
      mount_button(ancestor, i18n.block_btn, block_all_likers, success_notice(i18n.like_list_identifier, i18n.block_success))
    })

    waitForKeyElements('h2:has(> span:contains(' + i18n.retweet_title + '))', dom => {
      const ancestor = get_ancestor(dom, 3)
      mount_button(ancestor, i18n.block_btn, block_no_comment_retweeters, success_notice(i18n.retweet_list_identifier, i18n.block_success))
    })

    waitForKeyElements('h2:has(> span:contains(' + i18n.list_members + '))', dom => {
      const ancestor = get_ancestor(dom, 3)
      mount_button(ancestor, i18n.block_btn, block_list_members, success_notice(i18n.list_members_identifier, i18n.block_success))
    })
  }

  main()
})()