Thumb

4chan filter

By aeosynth Last update Nov 14, 2009 — Installed 24,017 times. Daily Installs: 68, 56, 279, 59, 54, 32, 46, 53, 38, 56, 61, 93, 75, 101, 51, 60, 48, 172, 47, 49, 138, 106, 90, 83, 68, 59, 50, 66, 46, 39, 48, 40

There are 62 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           4chan filter
// @namespace      http://userscripts.org/users/64431
// @description    Regular expression, point-and-click filtering
// @include        http://*.4chan.org/*
// @include        http://archive.easymodo.net/cgi-board.pl/*
// @include        http://4chanarchive.org/brchive/*
// @include        http://suptg.thisisnotatrueending.com/archive/*
// @version        6.1.0
// @copyright      2009, James Campos
// @license        GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @compatability  Firefox 3.5+, Opera 10+
// ==/UserScript==

//TODO
// fychan
// dimensions entering should have an immediate effect
// standardized movement
// object for filter lists
// pnc filtering doesn't work on easymodo
// new switches: -t temp, -F replies WITH files, -o -O op/not op (or reverse?)
// threading

(function() {// <- Opera wrapper

//define
function $(selector, root) {
	if (!root) root = document.body
	return root.querySelector(selector)
}

function $$(selector, root) {
	if (!root) root = document.body
	return root.querySelectorAll(selector)
}

function x(xpath, root) {
	if (!root) root = document.body
	return document.evaluate(xpath, root, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue
}

function X(xpath, root) {
	if (!root) root = document.body
	var result = document.evaluate(xpath, root, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null)
	var a = [], item
	while (item = result.iterateNext())
		a.push(item)
	return a
}

//main
if (!$('blockquote')) return

GM_addStyle("\
#thread_filter { color: inherit; text-align: right; font-size:16px; z-index:1; }\
#thread_filter > div:first-child { cursor: move; }\
#thread_filter > div:not(:first-child) > span,\
#thread_filter label,\
#thread_filter a\
 { cursor: pointer }\
#thread_filter .autohide { display: none }\
#thread_filter:hover .autohide,\
#thread_filter:active .autohide\
 { display: block }\
#thread_filter > div,\
#thread_filter.reply.autohide:not(:hover) > div\
 { padding: 0 5px 0 0; }\
#thread_filter.reply > div:first-child { padding: 10px 10px 0 10px }\
#thread_filter.reply > div:last-child { padding: 0 10px 10px 10px }\
#thread_filter.reply > div:not(:first-child):not(:last-child) { padding: 0 10px 0 10px }\
#thread_filter.reply:not(.autohide),\
#thread_filter.reply.autohide:hover\
 { border: 2px ridge; }\
#thread_filter.autohide:not(:hover)\
 { background: transparent; }\
")

var dialog = document.createElement('div')
dialog.innerHTML = '\
<div>\
	<span></span><br>\
	<span></span>\
</div>\
<div>\
	<select></select> <label>Fappe Tyme<input type="checkbox"></label><br>\
	<span>Name</span> <input><br>\
	<span>Tripcode</span> <input><br>\
	<span>Email</span> <input><br>\
	<span>Subject</span> <input><br>\
	<span>Comment</span> <input><br>\
	<span>File</span> <input><br>\
</div>\
<div style="display: none;">\
	<label>Min Width <input maxlength="4" size="2"></label><br>\
	<label>Min Height <input maxlength="4" size="2"></label><br>\
	<label>Manual Filtering<input type="checkbox"></label><br>\
	<label>Show Stubs<input type="checkbox"></label><br>\
	<label>Auto Tyme<input type="checkbox"></label><br>\
	<label>Alternate Skin<input type="checkbox"></label><br>\
	<label>Sticky<input type="checkbox"></label><br>\
	<input type="button" value="Clear Hidden">\
</div>\
<div>\
	<a title="The characters .*+?^${}()|[]/\\ are RegEx operators. Prefix them with a backslash to make them behave normally (? -> \\?).">hide</a> \
	<a title="Show filtered posts">show</a> \
	<a title="Switch between main/advanced controls">adv</a> \
	<a title="Auto-hide dialog box">auto</a>\
</div>'
dialog.id = 'thread_filter'
dialog.style.position = GM_getValue('Sticky', true) ? 'fixed' : 'absolute'
if (GM_getValue('right', true))
	dialog.style.right = GM_getValue('right', 0)
else
	dialog.style.left = GM_getValue('left')
if (GM_getValue('top', true))
	dialog.style.top = GM_getValue('top', 0)
else
	dialog.style.bottom = GM_getValue('bottom')

const reply = /res|read|archive\/|dspl_thread/.test(window.location.pathname)//imageboard|textboard|suptgarchive|4chanarchive
try{const site = window.location.hostname.match(/\.(\w+)/)[1]} catch(e){}// using try/catch because my saved test pages don't have url formatted filneames
const tripx = site == 'easymodo' ? ".//span[@class='postertrip']" : "./span[@class='postertrip']|./a/span[@class='postertrip']"
const filex = site == 'easymodo' ? "./span" : "./span[@class='filesize']"
const imagex = site == 'easymodo' ? ".//img[@class='thumb']" : ".//img[@md5]|//span[@class='tn_reply' or @class='tn_thread']"
switch (site) {
	case 'thisisnotatrueending':
		var board = '\/tg\/'
		break
	case 'easymodo':
		board = window.location.pathname.match(/\.pl(\/\w+)/)[1] + '/'
		break
	default: board = window.location.pathname.match(/(\/read)?(\/\w+\/)/)[2]
}
const manual = GM_getValue('Manual Filtering', true)
const stubs = GM_getValue('Show Stubs')
var replies = X('.//blockquote/parent::td')
var imageCount = X(imagex).length
var threads = X("./form/div[starts-with(@id, '4chan_ext_thread') or starts-with(@id, 'nothread')]")
var posts = $$('.post')//Text Boards
var hideCount = 0
var listBoard = board
var replyHidden = GM_getValue(board + 'reply', '')

var postCount = (posts.length || threads.length) ? posts.length + threads.length + replies.length : $$('blockquote').length

$('div:first-child', dialog).addEventListener('mousedown', startMove, true)
$('div:first-child > span:first-child', dialog).textContent = 'Images: ' + imageCount + ' Posts: ' + postCount

var list = $('select', dialog)
list.addEventListener('mouseup', loadValues, true)//the 'change' event is kinda weird.
list.addEventListener('keyup', loadValues, true)

var option = document.createElement('option')
option.textContent = 'global'
list.appendChild(option)
var option = document.createElement('option')
option.textContent = board
list.appendChild(option)
option.selected = true

var remainingBoards = GM_getValue('boardList', '').replace(board, '').match(/\/\w+\//g)
for (var i in remainingBoards) {
	var option = document.createElement('option')
	option.textContent = remainingBoards[i]
	list.appendChild(option)
}

var filters = $$('div:nth-child(2) input:not([type])', dialog)
for (var i = 0, l = filters.length; i < l; i++) {
	var span = filters[i].previousElementSibling
	span.addEventListener('click', expandInput, true)
	var name = span.textContent
	filters[i].value = GM_getValue(board + name, '')
	filters[i].addEventListener('keypress', function(event) { if (event.keyCode == 13 || event.keyCode == 14) applyF() }, true)
}

$('input[type=button]', dialog).addEventListener('click', function(){ GM_deleteValue(board + 'reply'); replyHidden = ''; }, true)

var boxes = $$('input[type="checkbox"]', dialog)
var hideText = boxes[0]// Expand Images can get added
if (GM_getValue('Auto Tyme'))
	hideText.checked = GM_getValue('Fappe Tyme')
hideText.addEventListener('click', switcher, true)
for (var i = 1, l = boxes.length; i < l; i++) {
	var text = boxes[i].previousSibling.nodeValue
	var defaultValue = false
	switch (text) {
		case 'Manual Filtering':
			defaultValue = true
			break
		case 'Sticky':
			defaultValue = true
			break
	}
	boxes[i].checked = GM_getValue(text, defaultValue)
	boxes[i].addEventListener('click', switcher, true)
}

var dimensions = $$('input[size]', dialog)
for (var i = 0, l = dimensions.length; i < l; i++) {
	dimensions[i].value = GM_getValue(dimensions[i].previousSibling.nodeValue.trim(), 0)
}

var anchors = $$('a', dialog)
anchors[0].addEventListener('click', applyF, true)
anchors[1].addEventListener('click', reset, true)
anchors[2].addEventListener('click', advF, true)
anchors[3].addEventListener('click', autoHideF, true)

var omitted = $$('.omittedposts')
for (var i = 0, l = omitted.length; i < l; i++) {//getPosts on thread collapse. Does this work w/ 4chan x?
	omitted[i].addEventListener('DOMCharacterDataModified', function() { if (/^\d/.test(this.textContent)) getPosts() }, true)//4chan extension
	var button = omitted[i].previousSiblingElement
	if (button)
		button.addEventListener('DOMAttrModified', function() { if (this.textContent=='+') getPosts() }, true)
}

if (manual)
	addReplyHiding(document.body)
document.body.addEventListener('DOMNodeInserted', function(e){if(e.target.nodeName=='TABLE') getPosts($('.reply', e.target))}, true)
autoHideF()
document.body.appendChild(dialog)
applyF()

//functions

function addReplyHiding(root) {
	var spans = $$('[id^=norep]', root)
	for (var i = 0, el; el = spans[i]; i++) {
		var toggle = document.createElement('a')
		toggle.style.cursor = 'pointer'
		toggle.className = 'pointer'
		toggle.addEventListener('click', handleReply, true)
		toggle.textContent = 'Hide'
		el.parentNode.insertBefore(toggle, el.nextSibling)
		el.parentNode.insertBefore(document.createTextNode(' '), toggle)
	}
}

function hideReply(toggle, bq, click) {
	if (stubs) {
		bq.style.display = 'none'
		toggle.textContent = 'Show'
		var prev = bq.previousSibling
		if (prev.href)//image
			prev.style.display = 'none'
	} else
		toggle.parentNode.parentNode.style.display = 'none'
	if (click) {
		replyHidden += 'norep' + bq.parentNode.id
		GM_setValue(board + 'reply', replyHidden)
	}
}

function showReply(toggle, bq, click) {
	bq.style.display = ''
	toggle.textContent = 'Hide'
	bq.previousSibling.style.display = ''
	if (click) {
		replyHidden = replyHidden.replace('norep' + bq.parentNode.id, '')
		GM_setValue(board + 'reply', replyHidden)
	}
}

function handleReply() {
	var bq = x("following::blockquote", this)
	if (this.textContent == 'Show')
		showReply(this, bq, true)
	else
		hideReply(this, bq, true)
}

function getPosts(el) {
	if (el) {
		if (manual)
			addReplyHiding(el)
		var tempImage = x(".//img[@md5]|.//span[@class='tn_reply']", el)
		if (tempImage)
			imageCount++
		if (hideText.checked && !tempImage) {
			el.parentNode.style.display = 'none'
			hideCount++
		} else if (spam(el)) {
			el.parentNode.style.display = 'none'
			hideCount++
		}
		replies.push(el)
		var hideSpan = $('div:first-child span:last-child', dialog)
		hideSpan.textContent = 'Hidden Posts: ' + hideCount
	} else {
		imageCount = X(imagex).length
		replies = X('.//blockquote/parent::td')
		hideCountF()
	}
	var postCount = reply ? replies.length + 1 : threads.length ? threads.length + replies.length : $$('blockquote').length
	$('div:first-child > span:first-child', dialog).textContent = 'Images: ' + imageCount + ' Posts: ' + postCount
}

function reset() {
	if (!reply)
		for (var i = 0; i < threads.length; i++) {
			var prev = threads[i].previousSibling
			//if (prev && prev.nodeName == 'HR' || /^\w/.test(prev.textContent)) {
			if (!prev || prev.nodeName == 'HR' || /^▲/.test(prev.textContent)) {
				threads[i].style.display = ''
				threads[i].nextSibling.style.display = ''
				threads[i].nextSibling.nextSibling.style.display = ''
			}
		}
	if (manual && stubs)
		for (var i in replies)
			showReply($('.pointer', replies[i]), $('blockquote', replies[i]))
	else
		for (var i in replies)
			replies[i].parentNode.style.display = ''
	for (var i = 0; i < posts.length; i++) {
		posts[i].parentNode.parentNode.style.display = ''
		posts[i].style.display = ''
	}
	if (hideText.checked)
		hideTextF(true)
	else if (this.nodeName)//Don't call hideCountF() in the middle of applyF()
		hideCountF()
}

function switcher() {
	var name = this.previousSibling.nodeValue
	var checked = this.checked
	GM_setValue(name, checked)
	switch(name) {
		case 'Alternate Skin':
			skinF(checked)
			break
		case 'Fappe Tyme':
			hideTextF(checked)
			break
		case 'Sticky':
			dialog.style.position = checked ? 'fixed' : 'absolute'
			break
		}
}

function hideTextF(checked) {
	if (checked) {
		const minWidth = GM_getValue('Min Width')
		const minHeight = GM_getValue('Min Height')
		for (var i in replies) {
			if (!x(filex, replies[i]))
				replies[i].parentNode.style.display = 'none'
			else if (site != 'easymodo') {
				var type = x("./span[@class='filesize']/a", replies[i])
				if (type.textContent.match(/[a-z]+/) == 'gif')
					continue
				var size = x("./span[@class='filesize']/text()[2]", replies[i])
				size = size.textContent.match(/(\d+)x(\d+)/)
				if (Number(size[1]) < minWidth)
					replies[i].parentNode.style.display = 'none'
				else if (Number(size[2]) < minHeight)
					replies[i].parentNode.style.display = 'none'
			}
		}
		hideCountF()
	} else {
		if (manual && stubs)
			for (var i in replies)
				replies[i].parentNode.style.display = ''
		else
			applyF()
	}
}

var specialCom
var specialTag
var names
var tripcodes
var emails
var subjects
var comments
var files
function applyF() {
	var prefix = list.value
	for (var i = 0, l = filters.length; i < l; i++)
		GM_setValue(prefix + filters[i].previousElementSibling.textContent, filters[i].value)
	for (var i = 0, l = dimensions.length; i < l; i++) {
		if (!dimensions[i].value.length || isNaN(dimensions[i].value) || dimensions[i].value < 0)
			dimensions[i].value = 0
		GM_setValue(dimensions[i].previousSibling.nodeValue.trim(), dimensions[i].value)
	}

	specialTag = []
	specialCom = []
	names = fetch('Name')
	tripcodes = fetch('Tripcode')
	emails = fetch('Email')
	subjects = fetch('Subject')
	comments = fetch('Comment')
	files = fetch('File')
	var nonEmpty

	function fetch(el) {
		var temp = GM_getValue(board + el)
		if (temp)
			nonEmpty = true
		else
			GM_deleteValue(board + el)
		temp += ';' + GM_getValue('global' + el)
		var rawMatches = temp.match(/[^\s;][^;]*/g)
		var filterList = new Array
		if (rawMatches) {
			if (el == 'Comment')
				for (var i in rawMatches) {
					var tempArray
					if (tempArray = rawMatches[i].match(/^(.+?) -([fhi]{1,3})$/)) {
						var tags = tempArray[2]
						var filter = new RegExp(tempArray[1], /i/.test(tags) ? '' : 'i')
						tags = tags.replace(/i/, '')
						if (tags.length) {
							specialTag.push(tags)
							specialCom.push(filter)
						} else
							filterList.push(filter)
					} else
						filterList.push(new RegExp(rawMatches[i], 'i'))
				}
			else
				for (var i in rawMatches) {
					if (tempArray = rawMatches[i].match(/(.+?) -i$/))
						filterList.push(new RegExp(tempArray[1], ''))
					else
						filterList.push(new RegExp(rawMatches[i], 'i'))
				}
			}
		return filterList
	}
	var boardList = GM_getValue('boardList', '')
	if (nonEmpty && prefix != 'global' && boardList.indexOf(prefix) == -1)
		GM_setValue('boardList', boardList + prefix)
	else if (!nonEmpty)
		GM_setValue('boardList', boardList.replace(prefix, ''))
	reset()
	if (!reply)
		for (var i in threads)
			if (spam(threads[i])) {
				threads[i].style.display = 'none'
				threads[i].nextSibling.style.display = 'none'
				threads[i].nextSibling.nextSibling.style.display = 'none'
			}
	if (manual && stubs) {
		for (var i in replies)
			if (spam(replies[i]))
				hideReply($('.pointer', replies[i]), $('blockquote', replies[i]))
	} else
		for (var i in replies)
			if (spam(replies[i]))
				replies[i].parentNode.style.display = 'none'
	for (var i = 0; i < posts.length; i++)
		if (spam(posts[i])) {
			if ($('.postnum', posts[i]).textContent == 1 && !reply)
				posts[i].parentNode.parentNode.style.display = 'none'
			else
				posts[i].style.display = 'none'
		}
	hideCountF()
}

function spam(el) {
	var temp = el.id
	if (temp && replyHidden.indexOf(temp) != -1)//text boards don't use id
		return true
	var bq = $('blockquote', el)
	bqTC = bq.textContent
	bqIH = bq.innerHTML
	for (var i in comments)
		if (comments[i].test(bqTC))
			return true
	if (specialCom)
		for (var i in specialCom) {
			if (/f/.test(specialTag[i]) && x(filex, el))
				continue
			temp = /h/.test(specialTag[i]) ? bqIH : bqTC
			if (specialCom[i].test(temp))
				return true
		}
	var n = x(".//span[@class='postername' or @class='commentpostername']", el)
	for (var i in names)
		if (names[i].test(n.textContent))
			return true
	if (files) {
		temp = x(filex, el)
		if (temp)
			for (j in files)
				if (files[j].test(temp.textContent))
					return true
	} if (tripcodes) {
		temp = x(tripx, el)
		if (temp)
			for (j in tripcodes)
				if (tripcodes[j].test(temp.textContent))
					return true
	} if (emails) {
		temp = $('a', n)
		if (temp)
			for (j in emails)
				if (emails[j].test(decodeURIComponent(temp.href.slice(7))))//slice off mailto:
					return true
	} if (subjects) {
		temp = el.nodeName == "DIV" ? $('.filetitle', el) : $('.replytitle', el)
		if (temp)
			for (j in subjects)
				if (subjects[j].test(temp.textContent))
					return true
	}
}

//GUI
var height
var initial_mouseX
var initial_mouseY
var initial_boxX
var initial_boxY
function startMove(event) {
	height = Math.min(document.documentElement.clientHeight, document.body.clientHeight)
	initial_mouseX = event.clientX
	initial_mouseY = event.clientY
	if (dialog.style.right)
		initial_boxX = parseInt(dialog.style.right)
	else
		initial_boxX = document.body.clientWidth - dialog.offsetWidth - parseInt(dialog.style.left)
	if (dialog.style.top)
		initial_boxY = parseInt(dialog.style.top)
	else
		initial_boxY = height - dialog.offsetHeight - parseInt(dialog.style.bottom)
	document.addEventListener('mousemove', move, true)
	document.addEventListener('mouseup', endMove, true)
}

function move(event) {
	var right = initial_boxX + initial_mouseX - event.clientX
	var left = document.body.clientWidth - dialog.offsetWidth - right
	dialog.style.left = ''
	if (left < right) {
		dialog.style.right = ''
		dialog.style.left = left + 'px'
		if (left < 25)
			dialog.style.left = 0
	} else if (right < 25)
		dialog.style.right = 0
	else
		dialog.style.right = right + 'px'

	var top = initial_boxY - initial_mouseY + event.clientY
	var bottom = height - dialog.offsetHeight - top
	dialog.style.bottom = ''
	if (bottom < top) {
		dialog.style.top = ''
		dialog.style.bottom = bottom + 'px'
		if (bottom < 25)
			dialog.style.bottom = 0
	} else if (top < 25)
		dialog.style.top = 0
	else
		dialog.style.top = top + 'px'
}

function endMove() {
	document.removeEventListener('mousemove', move, true)
	document.removeEventListener('mouseup', endMove, true)
	GM_setValue('right', dialog.style.right)
	GM_setValue('left', dialog.style.left)
	GM_setValue('top', dialog.style.top)
	GM_setValue('bottom', dialog.style.bottom)
}

function expandInput() {
	var input = this.nextElementSibling
	if (input.style.display != 'none') {
		input.style.display = 'none'
		var textArea = document.createElement('textarea')
		textArea.cols = 23
		textArea.rows = 5
		textArea.value = input.value
		textArea.addEventListener('change', function() { this.previousElementSibling.value = this.value }, true)
		this.parentNode.insertBefore(textArea, input.nextSibling)
		this.appendChild(document.createElement('br'))
		textArea.focus()
	} else {
		this.removeChild(this.lastChild)//remove <br>
		this.parentNode.removeChild(input.nextSibling)//remove textArea
		input.style.display = ''
		input.focus()
	}
}

function autoHideF() {
	var autoHide = $('a:nth-of-type(4)', dialog)
	var adv = $('a:nth-of-type(3)', dialog)
	var hideSpan = $('div:first-child span:last-child', dialog)
	var mainDiv = $('div:nth-child(2)', dialog)
	var advDiv = $('div:nth-child(3)', dialog)
	var buttonsDiv = $('div:last-child', dialog)
	var skin = GM_getValue('Alternate Skin')
	if (this.nodeName)
		GM_setValue('autohide', !GM_getValue('autohide'))
	if (GM_getValue('autohide')) {
		autoHide.innerHTML = '<b>auto</b>'
		hideSpan.className = 'autohide'
		adv.textContent == 'adv' ? mainDiv.className = 'autohide' : advDiv.className = 'autohide'
		buttonsDiv.className = 'autohide'
		dialog.className = skin ? 'autohide' : 'reply autohide'
	} else {
		autoHide.innerHTML = 'auto'
		hideSpan.className = ''
		mainDiv.className = ''
		advDiv.className = ''
		buttonsDiv.className = ''
		dialog.className = skin ? '' : 'reply'
	}
}

function advF() {
	var mainDiv = $('div:nth-child(2)', dialog)
	var advDiv = $('div:nth-child(3)', dialog)
	if (this.textContent == 'adv') {
		this.textContent = 'main'
		mainDiv.style.display = 'none'
		advDiv.style.display = ''
		if (GM_getValue('autohide')) {
			mainDiv.className = ''
			advDiv.className = 'autohide'
		}
	} else {
		this.textContent = 'adv'
		mainDiv.style.display = ''
		advDiv.style.display = 'none'
		if (GM_getValue('autohide')) {
			mainDiv.className = 'autohide'
			advDiv.className = ''
		}
	}
}

function skinF(useClear) {
	if (GM_getValue('autohide'))
		dialog.className = useClear ? 'autohide' : 'reply autohide'
	else
		dialog.className = useClear ? '' : 'reply'
}

function loadValues() {
	for (var i = 0, l = filters.length; i < l; i++)
		GM_setValue(listBoard + filters[i].previousElementSibling.textContent, filters[i].value)
	listBoard = list.value
	for (var i = 0, l = filters.length; i < l; i++)
		filters[i].value = GM_getValue(listBoard + filters[i].previousElementSibling.textContent, '')
	var textArea = $$('textarea', dialog)
	for (var i = 0, l = textArea.length; i < l; i++)
		textArea[i].value = textArea[i].previousSibling.value
}

function hideCountF() {
	hideCount = 0
	for (var i = 0; i < threads.length; i++)
		if (threads[i].style.display)
			hideCount++
	if (manual && stubs) {
		for (var i in replies)
			if ($('blockquote', replies[i]).style.display)
				hideCount++
	} else
		for (var i in replies)
			if (replies[i].parentNode.style.display)
				hideCount++
	for (var i = 0; i < posts.length; i++)
		if (posts[i].style.display)
			hideCount++

	$('span:last-child', dialog).textContent = 'Hidden Posts: ' + hideCount
}

}) ()