window.config = {
instance : "http://localhost:9149", auth : null, params_diff : null, send_host : true, cache_delay : true, hide_thread_count: true, debug: {
log: {
new_job_config : false,
api_request_info : false,
api_request_error: true,
api_response_info: false,
other_timing_info: false,
href_mutations : false,
max_json_size : false
}
}
};
window.cleaned_elements = new WeakMap(); window.too_big_elements = new WeakSet(); window.errored_elements = new WeakSet(); window.total_elements_cleaned = 0;
window.total_time_cleaning = 0;
async function main_loop() {
var elements = [...document.links]
.filter(e => !e.getAttribute("href").startsWith("#") && !window.cleaned_elements.has(e) && !window.too_big_elements.has(e) && !window.errored_elements.has(e) );
await clean_elements(elements);
setTimeout(main_loop, 100); }
async function clean_elements(elements, job_config) {
if (elements.length == 0) {return;}
job_config ??= await elements_to_job_config(elements);
if (JSON.stringify(job_config).length > window.MAX_JSON_SIZE) {
if (elements.length == 1) {
console.error(`[URLC] URL Cleaner element too big error: ${elements[0]}`);
window.too_big_elements.add(elements[0]);
return;
} else {
await clean_elements(elements.slice(0, elements.length/2), {...job_config, tasks: job_config.tasks.slice(0, job_config.tasks.length/2)});
elements = elements.slice(elements.length/2);
job_config.tasks = job_config.tasks.slice(job_config.tasks.length/2);
}
}
let start_time = new Date();
let id = Math.floor(Math.random()*1e8); let id_pad = " ".repeat(8-id.toString().length)
let last_time = start_time;
let now;
let data = JSON.stringify(job_config);
let done;
let doneawaiter = new Promise(resolve => {done = resolve;});
if (window.config.debug.log.new_job_config) {console.log("[URLC]"+id_pad, id, elements.length, "elements in", data.length, "bytes (", job_config, ")");}
await GM.xmlHttpRequest({
url: `${window.config.instance}/clean`,
method: "POST",
data: data,
timeout: 10000,
onabort : (e) => {if (window.config.debug.log.api_request_error) {now = new Date(); console.error("[URLC]"+id_pad, id, "abort took", now-last_time, "ms (", e, ")"); last_time = now;} done();},
onerror : (e) => {if (window.config.debug.log.api_request_error) {now = new Date(); console.error("[URLC]"+id_pad, id, "error took", now-last_time, "ms (", e, ")"); last_time = now;} done();},
onloadstart : (e) => {if (window.config.debug.log.api_request_info ) {now = new Date(); console.log ("[URLC]"+id_pad, id, "loadstart took", now-last_time, "ms (", e, ")"); last_time = now;}},
onloadprogress : (e) => {if (window.config.debug.log.api_request_info ) {now = new Date(); console.log ("[URLC]"+id_pad, id, "loadprogress took", now-last_time, "ms (", e, ")"); last_time = now;}},
onreadystatechange: (e) => {if (window.config.debug.log.api_request_info ) {now = new Date(); console.log ("[URLC]"+id_pad, id, "readystatechange took", now-last_time, "ms (", e, ")"); last_time = now;}},
ontimeout : (e) => {if (window.config.debug.log.api_request_error) {now = new Date(); console.error("[URLC]"+id_pad, id, "timeout took", now-last_time, "ms (", e, ")"); last_time = now;} done();},
onload: function(response) {
if (window.config.debug.log.api_response_info) {now = new Date(); console.log("[URLC]"+id_pad, id, "load took", now-last_time, "ms (", response, ")"); last_time = now;}
let result = JSON.parse(response.responseText);
if (result.Err == null) {
result.Ok.urls.forEach(function (cleaning_result, index) {
if (cleaning_result.Err == null) {
if (elements[index].href != cleaning_result.Ok) {
elements[index].setAttribute("href", cleaning_result.Ok);
}
window.cleaned_elements.set(elements[index], cleaning_result.Ok);
} else {
console.error("[URLC]"+id_pad, id, "DoTaskError:", cleaning_result.Err, "Element indesx:", index, "Element:", elements[index], "Task:", job_config.tasks[index]);
window.errored_elements.add(elements[index])
}
});
} else {
console.error("[URLC]"+id_pad, id, "job config error", result);
}
now = new Date();
window.total_time_cleaning += now-start_time;
window.total_elements_cleaned += elements.length;
if (window.config.debug.log.other_timing_info) {console.log("[URLC]"+id_pad, id, "writing took", now-last_time , "ms");}
if (window.config.debug.log.other_timing_info) {console.log("[URLC]"+id_pad, id, "all took", now-start_time, "ms");}
if (window.config.debug.log.other_timing_info) {console.log("[URLC]", "Total cleaning took", window.total_time_cleaning, "ms for", window.total_elements_cleaned, "elements");}
done();
}
});
await doneawaiter;
}
async function elements_to_job_config(elements) {
let ret = {
tasks: elements.map(x => element_to_task_config(x)),
context: await get_job_context(),
cache_delay: window.config.cache_delay,
cache_unthread: window.config.cache_unthread
};
if (window.config.auth) {
ret.auth = window.config.auth;
}
if (window.config.params_diff) {
ret.params_diff = window.config.params_diff;
}
return ret;
}
function element_to_task_config(element) {
if (/(^|\.)x\.com$/.test(window.location.hostname) && element.href.startsWith("https://t.co/") && element.innerText.startsWith("http")) {
return {
url: element.href,
context: {
vars: {
redirect_shortcut: element.childNodes[0].innerText + element.childNodes[1].textContent + (element.childNodes[2]?.innerText ?? "")
}
}
};
} else if (/(^|\.)allmylinks\.com$/.test(window.location.hostname) && element.pathname=="/link/out" && element.title.startsWith("http")) {
return {
url: element.href,
context: {
vars: {
redirect_shortcut: element.title
}
}
};
} else if (/(^|\.)furaffinity\.net$/.test(window.location.hostname) && element.matches(".user-contact-user-info a")) {
let url;
if (URL.canParse(element.href)) {
url = element.href;
} else {
url = "https://example.com/PARSE_URL_ERROR";
}
return {
url: url,
context: {
vars: {
contact_info_site_name: element.parentElement.querySelector("strong").innerHTML,
link_text: element.innerText
}
}
};
} else if (/(^|\.)bsky\.app$/.test(window.location.hostname) && element.getAttribute("href").startsWith("/profile/did:plc:") && element.innerText.startsWith("@")) {
return {
url: element.href,
context: {
vars: {
bsky_handle: element.innerText.replace("@", "")
}
}
};
} else if (/(^|\.)saucenao\.com$/.test(window.location.hostname) && /^https:\/\/(www\.)?(x|twitter)\.com\//.test(element.href)) {
return {
url: element.href,
context: {
vars: {
twitter_user_handle: element.parentElement.querySelector("[href*='/i/user/']").innerHTML.replace("@", "")
}
}
}
} else {
return element.href;
}
}
async function get_job_context() {
let ret = {};
if (window.config.send_host) {
ret.source_host = window.location.hostname;
}
return ret;
}
(async () => {
console.log(`[URLC] URL Cleaner Site Userscript loaded.
Licensed under the Affero General Public License V3 or later (SPDX: AGPL-3.0-or-later)
https://www.gnu.org/licenses/agpl-3.0.html
https://github.com/Scripter17/url-cleaner
`);
let done;
let doneawaiter = new Promise(resolve => {done = resolve;});
await GM.xmlHttpRequest({
url: `${window.config.instance}/get-max-json-size`,
method: "GET",
onerror: (e) => {console.error("[URLC] Error in getting the max JSON size:", e);},
onload: function(response) {
window.MAX_JSON_SIZE = parseInt(response.responseText);
done();
}
});
await doneawaiter;
new MutationObserver(function(mutations) {
if (window.config.debug.log.href_mutations) {console.log("[URLC]", "Href mutations observed (", mutations, ")");}
mutations.forEach(function(mutation) {
if (window.cleaned_elements.get(mutation.target) != mutation.target.href) {
window.cleaned_elements.delete(mutation.target);
window.too_big_elements.delete(mutation.target);
window.errored_elements.delete(mutation.target);
if (mutation.target.matches(":hover, :active, :focus, :focus-visible, :focus-within")) {
mutation.target.addEventListener("click", async function(e) {
if (window.cleaned_elements.has(e.target) || window.too_big_elements.has(e.target) || window.errored_elements.has(e.target)) {
return;
}
e.preventDefault();
try {
await clean_elements([e.target]);
} catch (err) {
console.error("[URLC] Error with handling an uncleaning clickjack:", e, err);
e.target.click();
throw err;
}
e.target.click();
}, {capture: true, once: true});
}
}
});
}).observe(document.documentElement, {
attributes: true,
attributeFilter: ["href"],
subtree: true
});
if (window.config.debug.log.max_json_size) {console.log("[URLC] max job config size is", window.MAX_JSON_SIZE, "bytes");}
await main_loop();
})();