Last active
May 24, 2024 08:47
-
-
Save mindbound/4e96536c3287f9acbcb9b7d0ed8909a9 to your computer and use it in GitHub Desktop.
LCF
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 1.0.0 | |
// @description Applies user-defined filters to the latest posts page | |
// @author Arets Paeglis <arets.paeglis@protonmail.com> (https://github.com/mindbound/) | |
// @copyright 2021+, Arets Paeglis (https://github.com/mindbound/) | |
// @license MIT; https://spdx.org/licenses/MIT.html | |
// @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://gist.github.com/mindbound/cb795d3b04756a953af30a403fa58416" target = "_blank">LCF ${GM_info.script.version}</a> | |
</p> | |
`, | |
events: { | |
open: function () { | |
makeDraggable(LCFConfig, LCFConfig_header); | |
LCFConfig_header.style.cursor = "move"; | |
toggleFilterMode(); | |
}, | |
save: function () { | |
applyFilters(); | |
} | |
}, | |
fields: { | |
mode: { | |
label: "Filter mode", | |
section: ["Mode", "Choose between blacklist and whitelist modes"], | |
type: "select", | |
options: ["Blacklist", "Whitelist"], | |
default: "Blacklist", | |
save: true | |
}, | |
blacklist: { | |
section: ["Filter List", "A list of user accounts to process"], | |
type: "textarea", | |
default: "", | |
save: true | |
}, | |
whitelist: { | |
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-family: sans-serif; | |
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: 25px; | |
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 toggleFilterMode() { | |
const whitelistField = document.getElementById("LCFConfig_field_whitelist").parentNode; | |
const blacklistField = document.getElementById("LCFConfig_field_blacklist").parentNode; | |
const optionsSection = document.getElementById("LCFConfig_section_2"); | |
if (cfg.get("mode") === "Whitelist") { | |
whitelistField.style.display = "block"; | |
blacklistField.style.display = "none"; | |
optionsSection.style.display = "none"; | |
} else { | |
whitelistField.style.display = "none"; | |
blacklistField.style.display = "block"; | |
optionsSection.style.display = "block"; | |
} | |
} | |
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(); | |
console.info("Selected WebGL backend for TensorFlow.js") | |
return; | |
} | |
console.warn("Failed to set WebGL backend for TensorFlow.js"); | |
const isWASMAvailable = await tf.setBackend("wasm"); | |
if (isWASMAvailable) { | |
await tf.ready(); | |
console.info("Selected WASM backend for TensorFlow.js") | |
return; | |
} | |
console.warn("Failed to set WASM backend for TensorFlow.js"); | |
const isCPUAvailable = await tf.setBackend("cpu"); | |
if (isCPUAvailable) { | |
await tf.ready(); | |
console.info("Selected CPU backend for TensorFlow.js") | |
return; | |
} | |
console.warn("Failed to set 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 applyBlacklistFilter() { | |
let filterList = cfg.get("blacklist"); | |
if (typeof filterList !== "string") { | |
console.error("Unexpected blacklist format"); | |
return; | |
} | |
filterList = filterList.split("\n").map(e => e.trim()).filter(e => e.length > 0); | |
applyFilterList(filterList); | |
} | |
function applyWhitelistFilter() { | |
let whitelist = cfg.get("whitelist"); | |
if (typeof whitelist !== "string") { | |
console.error("Unexpected whitelist format"); | |
return; | |
} | |
whitelist = whitelist.split("\n").map(e => e.trim()).filter(e => e.length > 0); | |
if (whitelist.length === 0) { | |
return; | |
} | |
const regexPatterns = whitelist.map(s => new RegExp(`\\b${s.replace(/\*/g, ".*").replace(/\+/g, ".+")}\\b`)); | |
const rows = Array.from(document.querySelectorAll("tr[valign='top']")); | |
rows.forEach(row => { | |
const href = row?.querySelector("td:nth-child(2) span.ljuser a")?.getAttribute("href") ?? ""; | |
const isAllowed = regexPatterns.some(pattern => pattern.test(href)); | |
if (!isAllowed) { | |
row.parentNode?.removeChild(row); | |
} | |
}); | |
} | |
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() { | |
if (cfg.get("mode") === "Blacklist") { | |
applyBlacklistFilter(); | |
if (cfg.get("filterNew")) { | |
applyNewUsersFilter(); | |
} | |
if (cfg.get("filterImages")) { | |
applyImagesFilter(); | |
} | |
if (cfg.get("filterWithML")) { | |
applyMLFilter(); | |
} | |
} else { | |
applyWhitelistFilter(); | |
} | |
} | |
function openConfig() { | |
cfg.open(); | |
const height = cfg.get("mode") === "Blacklist" ? 85 : 65; | |
LCFConfig.style = ` | |
height: ${height}%; | |
width: 400px; | |
max-height: 690px; | |
top: calc(50% - 350px); | |
left: calc(50% - 150px); | |
right: auto; | |
bottom: auto; | |
border: 1px solid #000000; | |
border-radius: 5px; | |
margin: 0px; | |
opacity: 1.0; | |
overflow: auto; | |
padding: 10px; | |
position: fixed; | |
z-index: 65536; | |
display: block; | |
`; | |
} | |
(function () { | |
GM_registerMenuCommand("Settings", openConfig); | |
applyFilters(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment