Manage e621/e926 blacklist and downvoted posts via Tampermonkey menu
// ==UserScript==
// @name e621 Downvotes & Blacklist Manager
// @namespace https://e621.net/
// @version 2.2
// @description Manage e621/e926 blacklist and downvoted posts via Tampermonkey menu
// @author Cinop
// @match https://e621.net/*
// @match https://e926.net/*
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const STORAGE_DOWNVOTES_KEY = 'e621_downvoted_posts';
const STORAGE_BLACKLIST_BACKUP_KEY = 'e621_clean_blacklist_backup';
const STORAGE_BLACKLIST_LOCKED_KEY = 'e621_load_blacklist_disabled';
const STORAGE_AUTO_UPDATE_KEY = 'e621_auto_update_enabled';
const STORAGE_AUTO_UPDATE_INIT_KEY = 'e621_auto_update_initialized';
let registeredMenuCommandIds = [];
// --- UI Toast notifications ---
function showToast(message, isError = false) {
console.log(`[e621 Manager] ${message}`);
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
z-index: 999999;
background: ${isError ? '#d32f2f' : '#1976d2'};
color: #fff;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: sans-serif;
font-size: 14px;
pointer-events: none;
transition: opacity 0.3s;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 4000);
}
// --- User & Page helpers ---
function getCurrentUser() {
let userId = document.body?.dataset?.userId;
let userName = document.body?.dataset?.userName;
const isAnonymous = document.body?.dataset?.userIsAnonymous === 'true';
if (!userId || !userName || isAnonymous) {
const siteUserElem = document.getElementById('site-user');
if (siteUserElem) {
try {
const data = JSON.parse(atob(siteUserElem.textContent.trim()));
if (data && data.name && !data.is?.anonymous) {
userId = String(data.id);
userName = data.name;
}
} catch (e) {
console.error('Failed to parse #site-user:', e);
}
}
}
if (!userId || !userName || isAnonymous) {
alert('Error: You are not logged in. Please sign in to e621/e926 first.');
return null;
}
return { userId, userName };
}
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content || '';
}
function getPostIdFromPage(el = null) {
if (el) {
const container = el.closest('[data-id], [data-post-id], article.thumbnail, article.post');
if (container) {
const id = container.dataset.id || container.dataset.postId;
if (id && /^\d+$/.test(id)) return Number(id);
}
}
const meta = document.querySelector('meta[name="post-id"]');
if (meta?.content && /^\d+$/.test(meta.content)) {
return Number(meta.content);
}
const imgContainer = document.getElementById('image-container');
if (imgContainer?.dataset.id) {
return Number(imgContainer.dataset.id);
}
const match = window.location.pathname.match(/\/posts\/(\d+)/);
return match ? Number(match[1]) : null;
}
// --- State Accessors (GM_* natively handles booleans and arrays) ---
const isAutoUpdateEnabled = () => GM_getValue(STORAGE_AUTO_UPDATE_KEY, false);
const setAutoUpdateEnabled = (val) => GM_setValue(STORAGE_AUTO_UPDATE_KEY, Boolean(val));
const isBlacklistLocked = () => GM_getValue(STORAGE_BLACKLIST_LOCKED_KEY, false);
const setBlacklistLocked = (val) => GM_setValue(STORAGE_BLACKLIST_LOCKED_KEY, Boolean(val));
// --- Fetch live blacklist ---
async function fetchLiveBlacklist(userId) {
const metaTag = document.querySelector('meta[name="blacklisted-tags"]');
if (metaTag?.content) {
try {
const parsed = JSON.parse(metaTag.content);
if (Array.isArray(parsed) && parsed.length > 0) return parsed;
} catch (e) {}
}
const res = await fetch(`/users/${userId}.json`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return (data.blacklisted_tags || '').split('\n').map(t => t.trim()).filter(Boolean);
}
// --- Save Blacklist and trigger native in-page UI update ---
async function saveAndApplyBlacklist(userId, userName, blacklistArray) {
const blacklistString = blacklistArray.join('\n');
let savedViaDialog = false;
try {
document.getElementById('blacklist-edit-link')?.click();
const textarea = document.getElementById('blacklist-edit');
const saveBtn = document.getElementById('blacklist-save');
if (textarea && saveBtn) {
textarea.value = blacklistString;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
saveBtn.click();
savedViaDialog = true;
setTimeout(() => {
const cancelBtn = document.getElementById('blacklist-cancel');
const uiClose = document.querySelector('.ui-dialog-titlebar-close');
(uiClose || cancelBtn)?.click();
}, 150);
}
} catch (e) {
console.warn('[e621 Manager] Dialog interaction failed, falling back to direct API', e);
}
if (savedViaDialog) {
const metaTag = document.querySelector('meta[name="blacklisted-tags"]');
if (metaTag) metaTag.content = JSON.stringify(blacklistArray);
} else {
const res = await fetch(`/users/${userId}.json`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRF-Token': getCsrfToken(),
'User-Agent': `Tampermonkey-DownvotesManager/2.2 (by ${userName} on e621)`
},
body: new URLSearchParams({ 'user[blacklisted_tags]': blacklistString }).toString()
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
location.reload();
}
}
// ==========================================
// 1. "load current blacklist" (Backup)
// ==========================================
async function loadCurrentBlacklist(silent = false) {
if (isBlacklistLocked()) {
const msg = 'Action disabled: downvotes are currently in the blacklist. Unload them first.';
showToast(msg, true);
if (!silent) alert(msg);
return;
}
const user = getCurrentUser();
if (!user) return;
try {
showToast('Fetching current blacklist...');
const blacklist = await fetchLiveBlacklist(user.userId);
GM_setValue(STORAGE_BLACKLIST_BACKUP_KEY, blacklist);
showToast(`Blacklist backup saved (${blacklist.length} entries)`);
if (!silent) alert(`Success!\nSaved ${blacklist.length} entries to backup.`);
} catch (err) {
showToast(`Error: ${err.message}`, true);
}
}
// ==========================================
// 2. "load downvotes"
// ==========================================
async function loadDownvotes(silent = false) {
const user = getCurrentUser();
if (!user) return;
try {
showToast('Fetching downvoted posts...');
const allDownvotedIds = new Set();
let lowestId = null;
const limit = 320;
while (true) {
let url = `/posts.json?tags=downvoted:${encodeURIComponent(user.userName)}&limit=${limit}`;
if (lowestId) url += `&page=b${lowestId}`;
const res = await fetch(url, {
headers: { 'User-Agent': `Tampermonkey-DownvotesManager/2.2 (by ${user.userName} on e621)` }
});
if (res.status === 429 || res.status === 503) {
await new Promise(r => setTimeout(r, 2500));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const posts = Array.isArray(data) ? data : (data.posts || []);
if (posts.length === 0) break;
for (const post of posts) allDownvotedIds.add(post.id);
lowestId = posts[posts.length - 1].id;
showToast(`Loaded ${allDownvotedIds.size} downvoted posts...`);
if (posts.length < limit) break;
await new Promise(r => setTimeout(r, 600));
}
const uniqueIds = Array.from(allDownvotedIds);
GM_setValue(STORAGE_DOWNVOTES_KEY, uniqueIds);
showToast(`Saved ${uniqueIds.length} downvoted posts`);
if (!silent) alert(`Success!\nFetched and stored ${uniqueIds.length} downvoted posts.`);
} catch (err) {
showToast(`Error: ${err.message}`, true);
}
}
// ==========================================
// 3. "upload downvotes to blacklist"
// ==========================================
async function uploadDownvotesToBlacklist(silent = false) {
const user = getCurrentUser();
if (!user) return;
const downvotedIds = GM_getValue(STORAGE_DOWNVOTES_KEY, []);
if (!downvotedIds.length) {
alert('Storage has no saved downvotes. Run "load downvotes" first.');
return;
}
try {
showToast('Comparing with live blacklist...');
const currentList = await fetchLiveBlacklist(user.userId);
const existingIds = new Set(
currentList
.map(item => item.trim().match(/^id:(\d+)$/i))
.filter(Boolean)
.map(m => Number(m[1]))
);
const toAdd = downvotedIds.filter(id => !existingIds.has(Number(id)));
setBlacklistLocked(true);
refreshMenu();
if (toAdd.length === 0) {
showToast('All downvotes already in blacklist');
if (!silent) alert('All downvoted posts are already present in your blacklist.');
return;
}
const updatedBlacklist = [...currentList, ...toAdd.map(id => `id:${id}`)];
showToast(`Adding ${toAdd.length} posts to blacklist...`);
await saveAndApplyBlacklist(user.userId, user.userName, updatedBlacklist);
showToast(`Added ${toAdd.length} posts to blacklist!`);
if (!silent) {
alert(`Done!\nAdded ${toAdd.length} entries to blacklist.\nTotal rules: ${updatedBlacklist.length}`);
}
} catch (err) {
showToast(`Error: ${err.message}`, true);
}
}
// ==========================================
// 4. "unload downvotes from blacklist"
// ==========================================
async function unloadDownvotesFromBlacklist(silent = false) {
const user = getCurrentUser();
if (!user) return;
const downvotedIds = GM_getValue(STORAGE_DOWNVOTES_KEY, []);
if (!downvotedIds.length) {
alert('Storage has no saved downvotes. Nothing to match for removal.');
return;
}
try {
showToast('Fetching blacklist for cleanup...');
const currentList = await fetchLiveBlacklist(user.userId);
const downvotedSet = new Set(downvotedIds.map(Number));
let removedCount = 0;
const updatedBlacklist = currentList.filter(item => {
const match = item.trim().match(/^id:(\d+)$/i);
if (match && downvotedSet.has(Number(match[1]))) {
removedCount++;
return false;
}
return true;
});
setBlacklistLocked(false);
refreshMenu();
if (removedCount === 0) {
showToast('No downvotes found in blacklist');
if (!silent) alert('No matching downvoted posts found in your current blacklist.');
return;
}
showToast(`Removing ${removedCount} posts from blacklist...`);
await saveAndApplyBlacklist(user.userId, user.userName, updatedBlacklist);
showToast(`Removed ${removedCount} posts`);
if (!silent) {
alert(`Done!\nRemoved ${removedCount} posts from blacklist.\nRemaining rules: ${updatedBlacklist.length}`);
}
} catch (err) {
showToast(`Error: ${err.message}`, true);
}
}
// ==========================================
// 5. "check downvotes via API" (Sync check)
// ==========================================
async function checkDownvotesViaApi() {
showToast('Syncing downvotes with blacklist via API...');
await loadDownvotes(true);
await uploadDownvotesToBlacklist(false);
}
// ==========================================
// 6. Toggle Auto Update Switch
// ==========================================
async function toggleAutoUpdate() {
const willEnable = !isAutoUpdateEnabled();
if (willEnable) {
const isFirstTime = !GM_getValue(STORAGE_AUTO_UPDATE_INIT_KEY, false);
if (isFirstTime) {
showToast('Initializing: loading downvotes...');
await loadDownvotes(true);
await uploadDownvotesToBlacklist(true);
GM_setValue(STORAGE_AUTO_UPDATE_INIT_KEY, true);
}
setAutoUpdateEnabled(true);
refreshMenu();
showToast('Auto Update enabled');
} else {
setAutoUpdateEnabled(false);
refreshMenu();
showToast('Auto Update disabled');
}
}
// --- Auto-update helper: Add single post ---
async function addPostToBlacklist(postId) {
const downvotedIds = new Set(GM_getValue(STORAGE_DOWNVOTES_KEY, []));
downvotedIds.add(postId);
GM_setValue(STORAGE_DOWNVOTES_KEY, Array.from(downvotedIds));
const user = getCurrentUser();
if (!user) return;
try {
const currentList = await fetchLiveBlacklist(user.userId);
const postTag = `id:${postId}`.toLowerCase();
const exists = currentList.some(t => t.trim().toLowerCase() === postTag);
if (!exists) {
currentList.push(`id:${postId}`);
await saveAndApplyBlacklist(user.userId, user.userName, currentList);
setBlacklistLocked(true);
refreshMenu();
showToast(`Auto Update: post #${postId} blacklisted`);
}
} catch (err) {
showToast(`Auto Update error: ${err.message}`, true);
}
}
// --- Auto-update helper: Remove single post ---
async function removePostFromBlacklist(postId) {
const downvotedIds = GM_getValue(STORAGE_DOWNVOTES_KEY, []).filter(id => Number(id) !== postId);
GM_setValue(STORAGE_DOWNVOTES_KEY, downvotedIds);
const user = getCurrentUser();
if (!user) return;
try {
const currentList = await fetchLiveBlacklist(user.userId);
const postTag = `id:${postId}`.toLowerCase();
const updatedList = currentList.filter(item => item.trim().toLowerCase() !== postTag);
if (updatedList.length !== currentList.length) {
await saveAndApplyBlacklist(user.userId, user.userName, updatedList);
showToast(`Auto Update: post #${postId} un-blacklisted`);
}
} catch (err) {
showToast(`Auto Update error: ${err.message}`, true);
}
}
// --- Vote Buttons Listener on Page ---
function setupVoteListener() {
document.addEventListener('click', async (e) => {
if (!isAutoUpdateEnabled()) return;
const voteBtn = e.target.closest('button[data-action="1"], button[data-action="-1"], .ptbr-vote-button');
if (!voteBtn) return;
if (voteBtn.classList.contains('comment-vote-down-link') || voteBtn.classList.contains('comment-vote-up-link')) return;
const action = voteBtn.dataset.action;
if (action !== '1' && action !== '-1') return;
const postId = getPostIdFromPage(voteBtn);
if (!postId) return;
const voteContainer = voteBtn.closest('.ptbr-vote') || document.querySelector('.ptbr-vote');
const currentVoteState = voteContainer ? String(voteContainer.dataset.vote || '0') : '0';
if (action === '-1') {
if (currentVoteState === '-1') {
await removePostFromBlacklist(postId);
} else {
await addPostToBlacklist(postId);
}
} else if (action === '1' && currentVoteState === '-1') {
await removePostFromBlacklist(postId);
}
}, true);
}
// ==========================================
// Tampermonkey Menu Setup
// ==========================================
function registerCommand(label, onClick) {
if (typeof GM_registerMenuCommand === 'function') {
const id = GM_registerMenuCommand(label, onClick);
if (id !== undefined) registeredMenuCommandIds.push(id);
}
}
function refreshMenu() {
if (typeof GM_unregisterMenuCommand === 'function') {
for (const id of registeredMenuCommandIds) {
try { GM_unregisterMenuCommand(id); } catch (e) {}
}
}
registeredMenuCommandIds = [];
if (isAutoUpdateEnabled()) {
registerCommand('Auto Update: ON (Click to Disable)', toggleAutoUpdate);
registerCommand(' ↳ Sync downvotes via API', checkDownvotesViaApi);
} else {
if (!isBlacklistLocked()) {
registerCommand('Backup current blacklist', () => loadCurrentBlacklist(false));
}
registerCommand('Load downvotes', () => loadDownvotes(false));
registerCommand('Upload downvotes to blacklist', () => uploadDownvotesToBlacklist(false));
registerCommand('Unload downvotes from blacklist', () => unloadDownvotesFromBlacklist(false));
registerCommand('Auto Update: OFF (Click to Enable)', toggleAutoUpdate);
}
}
// --- Initialization ---
setupVoteListener();
refreshMenu();
})();