Skip to content

Instantly share code, notes, and snippets.

@luavixen
Created January 4, 2023 08:02
Show Gist options
  • Save luavixen/f4e4f87e6d96bff5a4c549a8fea32d2a to your computer and use it in GitHub Desktop.
Save luavixen/f4e4f87e6d96bff5a4c549a8fea32d2a to your computer and use it in GitHub Desktop.
Tumblr userscript to automatically mass block users, visit your blog's settings to open the UI.
// ==UserScript==
// @name FoxBlock
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Tumblr userscript to automatically mass block users.
// @author [email protected]
// @match https://*.tumblr.com/*
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
if (!window.location.href.includes('tumblr.com/settings/blog/')) return;
const state = window?.['___INITIAL_STATE___'];
async function perform(currentBlog, targetBlog) {
const referrer = `https://www.tumblr.com/settings/blog/${currentBlog}`;
const authorization = 'Bearer ' + state.apiFetchStore.API_TOKEN;
let responseBackup;
let payloadBackup;
try {
responseBackup = await fetch(`https://www.tumblr.com/api/v2/blog/${currentBlog}/backup`, {
referrer, method: 'GET', mode: 'cors', credentials: 'include',
headers: {
'Authorization': authorization,
'Accept': 'application/json; format=camelcase',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
});
payloadBackup = await responseBackup.json();
} catch (cause) {
throw new Error('Request for CSRF token failed', { cause });
}
const authorizationCSRF = responseBackup.headers.get('x-csrf');
if (!authorizationCSRF) {
throw new Error('Response for CSRF token is invalid/errored');
}
let responseBlock;
let payloadBlock;
try {
responseBlock = await fetch(`https://www.tumblr.com/api/v2/blog/${currentBlog}/blocks`, {
referrer, method: 'POST', mode: 'cors', credentials: 'include',
headers: {
'Authorization': authorization,
'X-CSRF': authorizationCSRF,
'X-Ad-Blocker-Enabled': '0',
'X-Version': 'redpop/3/0//redpop/',
'Accept': 'application/json; format=camelcase',
'Content-Type': 'application/json; charset=utf8',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
},
body: JSON.stringify({ 'blocked_tumblelog': targetBlog, 'force': false }),
});
payloadBlock = await responseBlock.json();
} catch (cause) {
throw new Error('Request for block creation failed', { cause });
}
const meta = payloadBlock?.meta;
if (
typeof meta !== 'object'
|| typeof meta.msg !== 'string'
|| typeof meta.status !== 'number'
) {
throw new TypeError('Response from block creation is invalid');
}
return { msg: meta.msg, status: meta.status, ok: (meta.status >= 200 && meta.status < 300) };
}
const formatBlog = (text) => String(text).trim().toLowerCase();
const formatBlogList = (text) => Array.from(new Set(String(text).split(/[\s\n]+/).map(formatBlog).filter((text) => !!text)));
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"></head><body class="m-0 bg-transparent"><div id="foxblock" class="container-fluid m-4 p-3 bg-light text-body border rounded shadow" style="width:500px"><form id="foxblock-form"><div id="foxblock-content" class="vstack gap-3"><div><label for="foxblock-current" class="form-label">Current blog</label> <input id="foxblock-current" class="form-control" required placeholder="your-blog"></div><div><label for="foxblock-targets" class="form-label">Target blogs</label> <textarea id="foxblock-targets" class="form-control" rows="5" required placeholder="blog1&#10;blog2&#10;blog3&#10;" autocomplete="off" autocorrect="off" spellcheck="false"></textarea></div><div><label for="foxblock-interval" class="form-label">Request interval</label> <select id="foxblock-interval" class="form-select" required><option value="0">3 seconds</option><option value="1">2 seconds</option><option value="2">1 second</option><option value="3">0.5 seconds</option><option value="4">0.25 seconds</option></select></div><div><button id="foxblock-button" type="submit" class="btn btn-primary w-100"><span id="foxblock-button-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> <span>Block All</span></button></div><div><div id="foxblock-progress" class="progress" role="progressbar"><div id="foxblock-progress-bar" class="progress-bar" style="width:0%"></div></div></div><div id="foxblock-alerts" class="d-none"></div></div></form></div></body></html>`;
const frame = document.createElement('iframe');
frame.width = '100%';
frame.height = '100%';
frame.setAttribute('frameborder', '0');
frame.setAttribute('style', 'position: fixed; top: 0; left: 0; bottom: 0; right: 0; z-index: 999999999;');
frame.src = 'about:blank';
document.body.appendChild(frame);
const frameDocument = frame.contentWindow.document;
frameDocument.open();
frameDocument.write(html);
frameDocument.close();
const $form = frameDocument.getElementById('foxblock-form');
const $current = frameDocument.getElementById('foxblock-current');
const $targets = frameDocument.getElementById('foxblock-targets');
const $interval = frameDocument.getElementById('foxblock-interval');
const $button = frameDocument.getElementById('foxblock-button');
const $buttonSpinner = frameDocument.getElementById('foxblock-button-spinner');
const $progress = frameDocument.getElementById('foxblock-progress-bar');
const $alerts = frameDocument.getElementById('foxblock-alerts');
$current.value = '';
$targets.value = '';
$interval.value = '2';
const intervalValues = [3000, 2000, 1000, 500, 250];
function intervalWait() {
const ms = Number(intervalValues[parseInt($interval.value)]) || 1000;
return new Promise((resolve) => setInterval(resolve, ms));
}
function alert(message) {
const $alert = frameDocument.createElement('div');
$alert.className = 'alert alert-danger alert-dismissible fade show';
const $alertMessage = frameDocument.createElement('span');
$alertMessage.innerText = message;
const $alertButton = frameDocument.createElement('button');
$alertButton.className = 'btn-close';
$alertButton.type = 'button';
$alert.appendChild($alertMessage);
$alert.appendChild($alertButton);
function close() {
$alert.remove();
if ($alerts.getElementsByClassName('alert').length < 1) {
$alerts.classList.add('d-none');
}
}
$alertButton.addEventListener('click', close);
$alertButton.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter') close();
});
$alerts.appendChild($alert);
$alerts.classList.remove('d-none');
}
let locked = false;
async function executeStep(blogCurrent, blogTarget) {
try {
const meta = await perform(blogCurrent, blogTarget);
if (!meta.ok) {
alert(`${blogTarget}: ${meta.msg || 'Unknown error (remote)'}`);
}
} catch (err) {
console.error(err);
alert(`${blogTarget}: ${err.message || 'Unknown error (internal)'}`);
}
await intervalWait();
}
async function execute() {
if (locked) return;
locked = true;
$button.setAttribute('disabled', '');
$buttonSpinner.classList.remove('d-none');
try {
const blogCurrent = formatBlog($current.value);
const blogTargets = formatBlogList($targets.value);
if (!blogCurrent) {
alert('No current blog specified');
return;
}
if (blogTargets.length < 1) {
alert('No target blogs specified');
return;
}
$progress.style.width = '0%';
for (let i = 0, length = blogTargets.length; i < length; i++) {
await executeStep(blogCurrent, blogTargets[i]);
$progress.style.width = Math.floor(((i + 1) / length) * 100) + '%';
}
} finally {
$button.removeAttribute('disabled');
$buttonSpinner.classList.add('d-none');
locked = false;
}
}
$form.addEventListener('submit', (ev) => {
ev.preventDefault();
execute().catch((err) => console.error(err));
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment