Last active
April 14, 2024 00:32
-
-
Save mindbound/4e96536c3287f9acbcb9b7d0ed8909a9 to your computer and use it in GitHub Desktop.
Lielais Cibas Filtrs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name LCF | |
// @namespace https://gist.github.com/mindbound | |
// @version 0.5.4 | |
// @description Applies user-defined filters to the latest posts page | |
// @author mindbound | |
// @license WTFPL; http://www.wtfpl.net/ | |
// @match http://klab.lv/stats/latest.bml | |
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB7ElEQVQ4jWNgGF7AwEBA4M/zwPj/rwKX/3sesPb9Te/GqlSZ8hQ/oeJEb8HMSFe+SAdjXhsGBgY2DM0lUcI5O6ep/bx3yOb/g2MO/x+edP7/6LTL/wenXP4fWW36/8Q6s/8n15r8Pzhf+/+sMskdGAb05Mjlv9pm8P/tdq3/r7eq/3+1Re3/y00qGPj5OpX/m1plNqDrZ55Zpbvx3xnP/5/2Gv//tMfo/6e9Rv/f7dT9/2qz6v+Xm1T+P12j8v/hYtX/Dxaq/d9dp7yDgYGBCa47P0Kh8NdJz//fD1tjtRUd35un9r8xXCIRpp9l73STF//Pev1/s02LKAMeLVP9v6JA4RYDAwMjAysrg+GHA87//5xyJUrzy00q/x8uUf1/ql31PwMDgzEDJyeD9c/jbv9/HLUn2oC7c9T+X+vX+C/KzxzCwMDAwHdkjvH3j3uMidL8fL3K/+sTNf7vq1f+z8DAYAQJxBCBvmcblIky4M4siO25HiK7kaORrT5JZPvzDbg1vtgA0Xy+S/1/qa/YSQYGBlH0tMAR6cK3an2b1O8n6xCuebEREmgHmpX/14WIv9WS4VjMwMAggi0rcLIwMLgwMzAk8HMzddjqc8z3teZZ6W7Is1RXln0KExNDPgsDgzsDA4M+AwMDO0wTAOkmi+x6U2zHAAAAAElFTkSuQmCC | |
// @connect https://raw.githubusercontent.com | |
// @connect https://cdn.jsdelivr.net | |
// @require https://raw.githubusercontent.com/mindbound/GM_config/master/gm_config.js | |
// @require https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js | |
// @grant GM_xmlhttpRequest | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_registerMenuCommand | |
// @run-at document-end | |
// ==/UserScript== | |
"use strict"; | |
let model; | |
let isTFBackendInitialized = false; | |
const MODEL_NAME = "LCF"; | |
const MODEL_URL = "https://raw.githubusercontent.com/mindbound/LCF/master/model/model.json"; | |
const MODEL_VERSION_URL = "https://raw.githubusercontent.com/mindbound/LCF/master/model/model_version.txt" | |
const MODEL_PROD = true; | |
const chars = "abcdefghijklmnopqrstuvwxyz0123456789_"; | |
const charToInt = {}; | |
for (let i = 0; i < chars.length; i++) { | |
charToInt[chars[i]] = i + 1; | |
} | |
const cfg = new GM_configStruct({ | |
id: "LCFConfig", | |
title: ` | |
<p class = "lcftitle"> | |
<a href = "https://bit.ly/3Dhqkwl">LCF ${GM_info.script.version}</a> | |
</p> | |
`, | |
events: { | |
open: function () { | |
makeDraggable(LCFConfig, LCFConfig_header); | |
LCFConfig_header.style.cursor = "move"; | |
}, | |
save: function () { | |
applyFilters(); | |
} | |
}, | |
fields: { | |
filterList: { | |
section: ["Filter List", "A list of user accounts to filter"], | |
type: "textarea", | |
default: "", | |
save: true | |
}, | |
filterNew: { | |
label: "Filter all new users", | |
section: ["Options", "Additional filters and settings"], | |
type: "checkbox", | |
default: false, | |
save: true | |
}, | |
filterImages: { | |
label: "Filter all images", | |
type: "checkbox", | |
default: false, | |
save: true | |
}, | |
filterWithML: { | |
label: "ML-assisted filter", | |
type: "checkbox", | |
default: false, | |
save: true | |
} | |
}, | |
frame: document.body.appendChild(document.createElement("div")), | |
css: ` | |
#LCFConfig { | |
font-size: 13px; | |
background-color: #f3f6f4; | |
position: relative; | |
width: 350px; | |
margin: 0 auto; | |
} | |
#LCFConfig textarea { | |
font-family: monospace; | |
margin-left: auto; | |
margin-right: auto; | |
-webkit-box-sizing: border-box; | |
-moz-box-sizing: border-box; | |
box-sizing: border-box; | |
width: 100%; | |
height: 325px; | |
resize: none; | |
font-size: 12px; | |
} | |
#LCFConfig_header p.lcftitle { | |
font-size: 20px; | |
font-weight: bold; | |
margin-bottom: 20px; | |
user-select: none; | |
padding: 0px; | |
} | |
#LCFConfig .config_var { | |
position: relative; | |
} | |
#LCFConfig .field_label { | |
width: 100%; | |
padding-right: 30px; | |
box-sizing: border-box; | |
user-select: none; | |
} | |
#LCFConfig .field_label label { | |
font-size: 14px; | |
} | |
#LCFConfig .section_header { | |
font-size: 18px; | |
font-weight: bold; | |
user-select: none; | |
} | |
#LCFConfig .section_desc { | |
font-size: 14px; | |
font-style: italic; | |
user-select: none; | |
} | |
#LCFConfig input[type="checkbox"] { | |
position: absolute; | |
right: 250px; | |
top: 50%; | |
transform: translateY(-70%); | |
} | |
#LCFConfig .saveclose_buttons { | |
font-size: 14px; | |
padding: 5px 10px; | |
} | |
#LCFConfig .reset { | |
user-select: none; | |
} | |
` | |
}); | |
function makeDraggable(element, handle) { | |
if (!element) { | |
return; | |
} | |
let mdX = 0, mdY = 0, mdeX = 0, mdeY = 0; | |
const onMouseMove = (evt) => { | |
element.style.left = `${mdeX + evt.clientX - mdX}px`; | |
element.style.top = `${mdeY + evt.clientY - mdY}px`; | |
}; | |
const onMouseUp = () => { | |
document.removeEventListener("mousemove", onMouseMove); | |
document.removeEventListener("mouseup", onMouseUp); | |
}; | |
(handle ?? element).addEventListener("mousedown", function (evt) { | |
evt.preventDefault(); | |
this.style.userSelect = "none"; | |
mdX = evt.clientX; | |
mdY = evt.clientY; | |
mdeX = element.offsetLeft; | |
mdeY = element.offsetTop; | |
document.addEventListener("mousemove", onMouseMove); | |
document.addEventListener("mouseup", onMouseUp); | |
}); | |
} | |
function usernameToSequence(username, maxLength) { | |
let sequence = []; | |
for (let char of username) { | |
if (charToInt[char]) { | |
sequence.push(charToInt[char]); | |
} | |
} | |
return sequence.concat(Array(maxLength - sequence.length).fill(0)); | |
} | |
async function fetchRemoteModelVersion() { | |
try { | |
const response = await fetch(MODEL_VERSION_URL); | |
if (!response.ok) { | |
throw new Error(`Failed to fetch model version: ${response.statusText}`); | |
} | |
const version = await response.text(); | |
return version.trim(); | |
} catch (error) { | |
console.error(`Error fetching remote model version: ${error}`); | |
return null; | |
} | |
} | |
async function initializeTFBackend() { | |
if (typeof tf === "undefined") { | |
console.error("TensorFlow.js is not loaded"); | |
return; | |
} | |
if (MODEL_PROD) { | |
await tf.enableProdMode(); | |
} else { | |
await tf.enableDebugMode(); | |
} | |
const isWebGLAvailable = await tf.setBackend("webgl"); | |
if (isWebGLAvailable) { | |
await tf.ready(); | |
return; | |
} | |
console.warn("Failed to set the WebGL backend for TensorFlow.js"); | |
const isWASMAvailable = await tf.setBackend("wasm"); | |
if (isWASMAvailable) { | |
await tf.ready(); | |
return; | |
} | |
console.warn("Failed to set the WASM backend for TensorFlow.js"); | |
const isCPUAvailable = await tf.setBackend("cpu"); | |
if (isCPUAvailable) { | |
await tf.ready(); | |
return; | |
} | |
console.warn("Failed to set the CPU backend for TensorFlow.js"); | |
console.error("No suitable backend available"); | |
} | |
function applyFilterList(fl) { | |
if (fl.length === 0) { | |
return; | |
} | |
const regexPatterns = fl.map(s => new RegExp(`\\b${s.replace(/\*/g, ".*").replace(/\+/g, ".+")}\\b`)); | |
const rows = Array.from(document.querySelectorAll("tr[valign='top']")); | |
const rowsToRemove = []; | |
for (let tr of rows) { | |
const href = tr?.querySelector("td:nth-child(2) span.ljuser a")?.getAttribute("href") ?? ""; | |
for (let pattern of regexPatterns) { | |
if (pattern.test(href)) { | |
rowsToRemove.push(tr); | |
break; | |
} | |
} | |
} | |
for (let row of rowsToRemove) { | |
row.parentNode?.removeChild(row); | |
} | |
} | |
function applySpamFilter() { | |
let filterList = cfg.get("filterList"); | |
if (typeof filterList !== "string") { | |
console.error("Unexpected filter list format"); | |
return; | |
} | |
filterList = filterList.split("\n").map(e => e.trim()).filter(e => e.length > 0); | |
applyFilterList(filterList); | |
} | |
function applyNewUsersFilter() { | |
fetch("http://klab.lv/stats.bml") | |
.then((res) => { | |
if (!res.ok) { | |
throw new Error(`Bad network response: ${res.statusText}`); | |
} | |
return res.text(); | |
}) | |
.then((data) => { | |
try { | |
const dom = new DOMParser().parseFromString(data, "text/html"); | |
const filterList = Array.from( | |
dom.querySelectorAll("ul:nth-of-type(5) a") | |
).map((a) => { | |
const match = /([^\/]+)\/$/.exec(a.getAttribute("href")); | |
if (!match) { | |
throw new Error("Unexpected URL format"); | |
} | |
return match[1]; | |
}); | |
applyFilterList(filterList); | |
} catch (parseError) { | |
console.error(`Error while parsing or processing the data: ${parseError}`); | |
} | |
}) | |
.catch((fetchError) => { | |
console.error(`Error in fetch operation: ${fetchError}`); | |
}); | |
} | |
function applyImagesFilter() { | |
const images = document.querySelectorAll("img"); | |
if (images.length === 0) { | |
return; | |
} | |
const imagesToHide = []; | |
images.forEach((img) => { | |
imagesToHide.push(img); | |
}); | |
imagesToHide.forEach((img) => { | |
img.style.display = "none"; | |
}); | |
} | |
async function applyMLFilter() { | |
try { | |
if (!isTFBackendInitialized) { | |
await initializeTFBackend(); | |
isTFBackendInitialized = true; | |
} | |
const localVersion = localStorage.getItem("modelVersion"); | |
const remoteVersion = await fetchRemoteModelVersion(); | |
if (!localStorage[`tensorflowjs_models/${MODEL_NAME}/model_topology`] || (remoteVersion && (!localVersion || remoteVersion > localVersion))) { | |
model = await tf.loadLayersModel(MODEL_URL); | |
await model.save(`localstorage://${MODEL_NAME}`); | |
localStorage.setItem("modelVersion", remoteVersion); | |
} else if (!model) { | |
model = await tf.loadLayersModel(`localstorage://${MODEL_NAME}`); | |
} | |
const response = await fetch("http://klab.lv/stats/latest.bml"); | |
if (!response.ok) { | |
throw new Error(`Bad network response: ${response.statusText}`); | |
} | |
const data = await response.text(); | |
const maxLength = 16; | |
const regex = /<span class='ljuser' style='white-space: nowrap;'>.*?<b>(.*?)<\/b><\/a><\/span>/g; | |
const usernames = []; | |
let match; | |
while ((match = regex.exec(data)) !== null) { | |
usernames.push(match[1]); | |
} | |
const usernameSequences = usernames.map(username => usernameToSequence(username, maxLength)); | |
tf.tidy(() => { | |
const inputTensor = tf.tensor(usernameSequences); | |
const outputTensor = model.predict(inputTensor); | |
outputTensor.data().then(predictions => { | |
const filterList = usernames.filter((username, idx) => predictions[idx] > 0.5); | |
applyFilterList(filterList); | |
}); | |
}); | |
} catch (error) { | |
console.error(`Error in ML filter: ${error}`); | |
} | |
} | |
function applyFilters() { | |
applySpamFilter(); | |
cfg.get("filterNew") && applyNewUsersFilter(); | |
cfg.get("filterImages") && applyImagesFilter(); | |
cfg.get("filterWithML") && applyMLFilter(); | |
} | |
function openConfig() { | |
cfg.open(); | |
LCFConfig.style = ` | |
height: 75%; | |
max-height: 620px; | |
top: calc(50% - 350px); | |
width: 400px; | |
left: calc(50% - 150px); | |
border: 1px solid #000000; | |
border-radius: 5px; | |
margin: 0px; | |
opacity: 1.0; | |
overflow: auto; | |
padding: 10px; | |
position: fixed; | |
z-index: 65536; | |
display: block; | |
right: auto; | |
bottom: auto; | |
`; | |
} | |
(function () { | |
GM_registerMenuCommand("Settings", openConfig); | |
applyFilters(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment