Skip to content

Instantly share code, notes, and snippets.

@mindbound
Last active April 14, 2024 00:32
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mindbound/4e96536c3287f9acbcb9b7d0ed8909a9 to your computer and use it in GitHub Desktop.
Save mindbound/4e96536c3287f9acbcb9b7d0ed8909a9 to your computer and use it in GitHub Desktop.
Lielais Cibas Filtrs
// ==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