// ==UserScript==
// @name allen's block
// @namespace https://eolstudy.com
// @version 3.0.1
// @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()
})()