// ==UserScript==
// @name 4chan md5 example
// @namespace lig
// @license WTFPL
// @description Example of how to compute the same md5 hashes that 4chan does, allows filters to be carried over to other sites
// @version 1.0.0
// @match *://boards.4chan.org/*/thread/*
// @match *://boards.4channel.org/*/thread/*
// @match *://boards.4chan.org/*/
// @match *://boards.4channel.org/*/
// @match *://8chan.moe/*
// @match *://8chan.se/*
// @run-at document-start
// @grant GM_xmlhttpRequest
// @require https://raw.github.com/emn178/js-md5/master/build/md5.min.js
// ==/UserScript==
const debug_GM_fetch = false
function parseHeaders(responseHeaders) {
var head = new Headers()
var pairs = responseHeaders.trim().split('\n')
pairs.forEach(function(header) {
var split = header.trim().split(':')
var key = split.shift().trim()
var value = split.join(':').trim()
try {
head.append(key, value)
} catch(e) {
console.error(e);
}
})
return head
}
function GM_fetch(url, options = {}) {
return new Promise((res, rej) => {
const host = new URL(url).hostname;
options.url = url;
options.method = options.method || 'GET';
options.responseType = 'text';
options.headers = options.headers || {};
options.headers.userAgent
Object.assign(options.headers, {
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.5",
"alt-used": host,
"cache-control": "no-cache",
connection: "keep-alive",
host: host,
pragma: "no-cache",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
//"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0"
})
options.onload = _res => {
const parsedHeaders = parseHeaders(_res.responseHeaders);
if(debug_GM_fetch) {
console.log('parsedHeaders', parsedHeaders);
console.log('response', _res);
}
const response = new Response(_res.response, {
status: _res.status,
statusText: _res.statusText,
headers: parsedHeaders
})
Object.defineProperty(response, "url", { value: url });
res(response);
};
options.onerror = function() {
setTimeout(function() {
rej(new TypeError('Network request failed'))
}, 0)
}
options.ontimeout = function() {
setTimeout(function() {
rej(new TypeError('Network request timed out'))
}, 0)
}
options.onabort = function() {
setTimeout(function() {
rej(new DOMException('Aborted', 'AbortError'))
}, 0)
}
GM_xmlhttpRequest(options);
});
}
function pack(argument, reverse = false) {
let result = '';
if(argument.length && typeof argument[0] == 'number')
for(let i = 0; i < argument.length; i++)
argument[i] = String.fromCharCode(argument[i]);
//console.log('argument', argument);
for(let i = 0; i < argument.length; i += 2) {
// Always get per 2 bytes...
let word = argument[i]
if (((i + 1) >= argument.length) || typeof argument[i + 1] === 'undefined') {
word += '0'
} else {
word += argument[i + 1]
}
// The fastest way to reverse?
if(reverse) {
word = word[1] + word[0]
}
result += String.fromCharCode(parseInt(word, 16))
}
return result;
}
unsafeWindow.getMD5 = async url => {
const buffer = await GM_fetch(url).then(r => r.arrayBuffer());
let digest = md5(buffer);
return btoa(pack(digest));
}