Something went wrong. Try again.
this post sux
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618/* * sux content script (isolated world) for bsky.app. * * - Discovers posts from the rendered feed DOM (permalink anchors + item text). * This is transport-agnostic: bsky.app's API client uses a captured fetch * reference that page-world wrappers can't see, so network interception is * only a supplement, not the source of truth. * - Feed items are identified by their action bar (Reply/Like/Repost buttons): * quote embeds and reply previews have none, so nested anchors resolve to the * wrapping item. An item hides when its own verdict is hide OR when any post * it quotes/previews (nested) is hide — no empty husks. * - Hides items while the filter is active unless the verdict is "show" * (hide-until-judged); when off, every trace is removed. * - Mirrors diagnostics into the page world (__suxDiagJSON). *//* global SuxShared, browser */(function () { "use strict";
var SH = globalThis.SuxShared; var POSTS_CAP = 8000; var HIDDEN_ATTR = "data-sux-hidden"; var HIDDEN_CLASS = "sux-hidden"; var PEEK_CLASS = "sux-peeked"; var BAR_CLASS = "sux-peekbar"; var ACTION_SEL = '[aria-label^="Reply"], [aria-label^="Like"], [aria-label^="Repost"]';
/* ---- diagnostics (mirrored to page world + sent to background) ---- */ var diag = { seen: 0, // posts received from the interceptor (supplement) domExtracted: 0, // posts extracted from the DOM items: 0, // feed item elements currently tracked hidden: 0, // of those, currently hidden unmatchedAnchors: 0, // feed items we could not map to a post path nested: 0, // nested (quote/reply-preview) links on tracked items scans: 0, settingsActive: false, interceptorInjected: false, near50: 0, near50Samples: "", };
function publishDiag() { try { if (window.wrappedJSObject) { // Primitives/strings pass the cross-world boundary; objects don't. window.wrappedJSObject.__suxDiagJSON = JSON.stringify(diag); } } catch (e) { /* ignore */ } }
/* ---- inject page-world fetch interceptor (supplement only) ---- */ var inject = document.createElement("script"); inject.src = browser.runtime.getURL("interceptor.js"); inject.onload = function () { diag.interceptorInjected = true; console.debug("[sux] interceptor injected"); publishDiag(); inject.remove(); }; (document.head || document.documentElement).appendChild(inject);
/* ---- our hiding style ---- */ var style = document.createElement("style"); style.id = "sux-style"; style.textContent = "." + HIDDEN_CLASS + "{display:none!important}" + /* SUX-6 peek: the item stays in the flow, children collapse to a bar */ "." + PEEK_CLASS + "{display:block!important;overflow:hidden;max-height:34px;opacity:.75}" + "." + PEEK_CLASS + " *{visibility:hidden}" + "." + PEEK_CLASS + " ." + BAR_CLASS + "{visibility:visible}" + "." + BAR_CLASS + "{display:flex;align-items:center;gap:8px;padding:6px 10px;" + "font:12px/1.4 system-ui,sans-serif;color:#859289;cursor:default}" + "." + BAR_CLASS + " button{visibility:visible;background:none;border:1px solid #475258;" + "border-radius:6px;color:#7fbbb3;padding:1px 8px;font:11px system-ui,sans-serif;cursor:pointer}" + "." + BAR_CLASS + " button:hover{border-color:#7fbbb3}"; document.documentElement.appendChild(style);
/* ---- state ---- */ var settings = SH.defaults(); var active = false; var port = null; var gen = -1; // background's settings generation we're showing
var posts = new Map(); // post path ("profile/<author>/post/<rkey>") -> {id, text, alt} var verdicts = new Map(); // post path -> "show" | "hide" var scores = new Map(); // post path -> {p0?, n0?, ...} judgment scores (telemetry) var elements = new Map(); // feed item element -> {main, nested: []} var askedGen = new Map(); // post path -> generation it was last requested under var applied = new WeakMap(); // feed item element -> state applyFor last rendered var itemOf = new WeakMap(); // anchor -> its feed item (itemContainer is a DOM climb)
/* ---- post indexing ---- */
function indexPost(p) { if (!p || !p.id || posts.has(p.id)) return false; posts.set(p.id, p); if (posts.size > POSTS_CAP) { var old = posts.keys().next().value; posts.delete(old); askedGen.delete(old); } return true; }
// First usable score among enabled valid clauses: rule B nouls preferred, // then rule A probabilities. Detail keys are index-aligned with the clauses // (n0, n1, … / p0, p1, …), so a missing answer just falls through. function primaryScore(st, d) { var i, c; for (i = 0; i < st.rulesB.length; i++) { c = st.rulesB[i]; if (c.enabled && SH.clauseBValid(c) && typeof d["n" + i] === "number") return d["n" + i]; } for (i = 0; i < st.rulesA.length; i++) { c = st.rulesA[i]; if (c.enabled && SH.clauseAValid(c) && typeof d["p" + i] === "number") return d["p" + i]; } return null; }
/* ---- background connection ---- */
function requestVerdicts(list) { if (!port || !list.length) return; list.forEach(function (p) { askedGen.set(p.id, gen); }); try { port.postMessage({ type: "getVerdicts", posts: list.map(function (p) { return { id: p.id, text: p.text, alt: p.alt }; }), }); } catch (e) { /* port is gone; reconnect will resend */ } }
function connect() { port = browser.runtime.connect({ name: "sux" }); port.onMessage.addListener(function (msg) { if (!msg) return; if (msg.type === "settings") { settings = SH.normalizeSettings(msg.settings); active = !!msg.active; gen = msg.gen; diag.settingsActive = active; // Old verdicts stay on screen until the new ones land — re-hiding // everything would blank the feed and refill it post by post. // askedGen no longer matches, so each post re-requests once. publishDiag(); reconcile(); } else if (msg.type === "verdicts") { if (msg.gen !== gen) return; // judged under rules we've since left (msg.items || []).forEach(function (it) { verdicts.set(it.id, it.verdict); if (it.d) { scores.set(it.id, it.d); if (scores.size > 1000) scores.delete(scores.keys().next().value); } }); applyVerdicts(); } }); port.onDisconnect.addListener(function () { port = null; setTimeout(connect, 1000); }); requestVerdicts(onScreenPosts()); } connect();
setInterval(function () { publishDiag(); if (port) { try { port.postMessage({ type: "contentStats", stats: diag }); } catch (e) { /* ignore */ } } }, 2000);
/* ---- posts from the page-world interceptor (supplement) ---- */ /* Converted from at:// URIs to the same path id space used by the DOM scan. */
function pathFromAtUri(uri) { var m = String(uri).match(/^at:\/\/([^/]+)\/app\.bsky\.feed\.post\/([^/]+)$/); return m ? "profile/" + m[1].toLowerCase() + "/post/" + m[2] : null; }
window.addEventListener("message", function (ev) { if (ev.source !== window || !ev.data || ev.data.source !== "sux-interceptor") return; var list = ev.data.posts || []; diag.seen += list.length; var fresh = []; for (var i = 0; i < list.length; i++) { var path = pathFromAtUri(list[i].id); if (!path) continue; var post = { id: path, text: list[i].text || "", alt: list[i].alt || "" }; if (indexPost(post)) fresh.push(post); } publishDiag(); if (fresh.length) { console.debug("[sux] interceptor sent " + fresh.length + " new posts"); requestVerdicts(fresh); } });
/* ---- DOM mapping ---- */
// "/profile/<author>/post/<rkey>" from either relative or absolute hrefs, else null. function normalizePostPath(href) { if (!href || typeof href !== "string") return null; var h = href.replace(/^https?:\/\/[^/]+/i, "").split("?")[0].split("#")[0]; var m = h.match(/^\/profile\/([^/]+)\/post\/([^/]+)\/?$/); if (!m) return null; return "profile/" + m[1].toLowerCase() + "/post/" + m[2]; }
function normalizeProfilePath(href) { if (!href || typeof href !== "string") return null; var h = href.replace(/^https?:\/\/[^/]+/i, "").split("?")[0].split("#")[0]; var m = h.match(/^\/profile\/([^/]+)\/?$/); return m ? m[1].toLowerCase() : null; }
// A feed item is the ancestor whose parent holds at least two action-bar- // bearing children (sibling items in the virtualized list). Quote embeds and // reply previews have no action bar of their own, so every anchor in the item // — main or nested — climbs to the same wrapper. Probe-validated: returns the // same node as the legacy heuristic on plain posts, and the whole item (not // the embed box) on quote posts, which is what prevents empty husks. function itemContainer(anchor) { var node = anchor.parentElement; for (var depth = 0; node && node !== document.body && depth < 14; depth++) { var parent = node.parentElement; if (parent) { var kidsWithBars = 0; for (var i = 0; i < parent.children.length; i++) { if (parent.children[i].querySelector(ACTION_SEL)) kidsWithBars++; } if (kidsWithBars >= 2) return node; } node = parent; } return null; }
// Smallest block holding this anchor but no sibling post anchor — for a // nested (quote / reply-preview) anchor that is the embed box itself. Used // to extract the nested post's own text so it can be judged even when it // never appears as a standalone feed item. function nestedContainer(anchor) { var node = anchor.parentElement; for (var depth = 0; node && node !== document.body && depth < 12; depth++) { var parent = node.parentElement; if (parent) { var siblings = parent.querySelectorAll('a[href*="/post/"]'); for (var i = 0; i < siblings.length; i++) { if (!node.contains(siblings[i])) return node; } } node = parent; } return anchor.parentElement; }
function depthWithin(root, el) { var d = 0; while (el && el !== root) { d++; el = el.parentElement; } return d; }
// Which post does this item represent? Nested anchors (quote embeds, reply // parents) belong to other posts. The main post's permalink is the anchor // whose author matches the item's dominant profile links (avatar, name, // handle all point at the main author); ties break on shallowest depth, // then document order. function chooseMainAnchor(item, anchors) { var counts = new Map(); var profileLinks = item.querySelectorAll('a[href*="/profile/"]'); for (var i = 0; i < profileLinks.length; i++) { var pp = normalizeProfilePath(profileLinks[i].getAttribute("href")); if (pp) counts.set(pp, (counts.get(pp) || 0) + 1); } var dominant = null; var best = 0; counts.forEach(function (n, author) { if (n > best) { best = n; dominant = author; } }); var fallback = null; var bestMatch = null; for (var j = 0; j < anchors.length; j++) { var a = anchors[j]; var path = normalizePostPath(a.getAttribute("href")); if (!path) continue; var rec = { a: a, depth: depthWithin(item, a), order: j }; if (!fallback || rec.depth < fallback.depth) fallback = rec; if (dominant && path.slice("profile/".length).split("/")[0] === dominant) { if (!bestMatch || rec.depth < bestMatch.depth) bestMatch = rec; } } var winner = bestMatch || fallback; return winner ? winner.a : null; }
/* ---- DOM text extraction ---- */
var ALT_CAP = 4000;
function itemAlts(item) { var out = []; var imgs = item.querySelectorAll("img"); for (var i = 0; i < imgs.length; i++) { var a = imgs[i].alt; if (a && a.trim()) out.push(a.trim()); } return out.join("\n").slice(0, ALT_CAP); }
// Header lines (repost credit, display name, handle, "· 19h") and the // trailing action-bar counts are noise for Jev; keep the post's own words. function cleanItemText(item) { var raw = item.innerText || ""; if (!raw.trim()) return ""; var lines = raw.split("\n"); var cut = -1; for (var i = 0; i < lines.length && i < 14; i++) { if (/^\s*·\s/.test(lines[i])) { cut = i; break; } } var body = cut >= 0 ? lines.slice(cut + 1) : lines; body = body.filter(function (l) { return !/^\s*reposted by\b/i.test(l); }); while (body.length) { var last = body[body.length - 1].trim(); if (last === "" || /^\d+([.,]\d+)?\s*[kKmM]?$/.test(last)) { body.pop(); } else break; } return body.join("\n").trim().slice(0, SH.TEXT_CAP); }
/* ---- hiding ---- */
function setHidden(el, hide) { if (hide) { el.setAttribute(HIDDEN_ATTR, "1"); el.classList.add(HIDDEN_CLASS); } else { el.removeAttribute(HIDDEN_ATTR); el.classList.remove(HIDDEN_CLASS); } }
/* ---- SUX-6: peek mode ---- * * When settings.peek is on, a hidden item collapses to a one-line bar * (rule, score, two correction buttons) instead of vanishing. Corrections * go to the background as {type:"correct"}; the changed clause hash makes * every cacheKey new, so the re-judgment arrives as a normal verdicts * broadcast — no reload. */
function removePeekBar(item) { var bar = item.querySelector("." + BAR_CLASS); if (bar) bar.remove(); item.classList.remove(PEEK_CLASS); }
function correct(kind, ruleIndex, post, btn) { btn.disabled = true; browser.runtime.sendMessage({ type: "correct", kind: kind, ruleIndex: ruleIndex, postId: post.id, text: post.text, alt: post.alt, }).then(function (res) { if (res && res.ok) { btn.textContent = "saved — re-judging…"; } else { btn.textContent = (res && res.error) || "failed"; btn.disabled = false; } }).catch(function () { btn.textContent = "failed"; btn.disabled = false; }); }
// One-line placeholder with the hiding rule, its score, and the two // correction actions. Only example rules get buttons — word rules have no // counter-example slot to correct into. function addPeekBar(item, rec) { var post = posts.get(rec.main); var d = scores.get(rec.main) || {}; var hid = SH.hiddenByClause(settings, d); var bar = document.createElement("div"); bar.className = BAR_CLASS; bar.setAttribute("data-sux-peek", "1"); var label = document.createElement("span"); label.className = "sux-peeklabel"; label.textContent = "sux hid this" + (hid ? " — " + hid.name + " " + (Math.round(hid.score * 100) / 100) : ""); bar.appendChild(label); if (hid && hid.type === "E" && post) { var notThis = document.createElement("button"); notThis.type = "button"; notThis.className = "sux-correct"; notThis.title = "This post is not like the examples — remember that"; notThis.textContent = "not this"; notThis.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); correct("not-this", hid.index, post, notThis); }); var more = document.createElement("button"); more.type = "button"; more.className = "sux-correct"; more.title = "Hide more posts like this one"; more.textContent = "more like this"; more.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); correct("more-like", hid.index, post, more); }); bar.appendChild(notThis); bar.appendChild(more); } item.appendChild(bar); item.classList.add(PEEK_CLASS); }
// An item hides when its own post is not "show" (hide-until-judged), or when // any post it quotes/previews has an explicit "hide" verdict — no husks. // Peek mode collapses judged hides to the correction bar; pending posts and // nested-quote hides stay fully hidden (a bar for "waiting on Jev" or for // someone else's quote would be noise). // // Runs on every scan, so it only touches the DOM when the wanted state // differs from what's there: rewriting an identical peek bar is a mutation, // which would schedule another scan, forever. function applyFor(item, rec) { var want = "show"; var own = verdicts.get(rec.main); if (active) { var hide = own !== "show"; if (!hide && rec.nested) { for (var i = 0; i < rec.nested.length; i++) { if (verdicts.get(rec.nested[i]) === "hide") { hide = true; break; } } } if (hide && settings.peek && own === "hide") { var hid = SH.hiddenByClause(settings, scores.get(rec.main) || {}); want = "peek:" + rec.main + ":" + (hid ? hid.type + hid.index + ":" + hid.score : ""); } else if (hide) { want = "hide"; } } // The page can re-render our marks away; check the DOM, not just memory. var bar = item.querySelector("." + BAR_CLASS); var intact = want === "show" ? !bar && !item.classList.contains(HIDDEN_CLASS) : want === "hide" ? !bar && item.classList.contains(HIDDEN_CLASS) : !!bar && item.classList.contains(PEEK_CLASS); if (intact && applied.get(item) === want) return; applied.set(item, want); removePeekBar(item); if (want.indexOf("peek:") === 0) { setHidden(item, false); addPeekBar(item, rec); } else { setHidden(item, want === "hide"); } }
function applyVerdicts() { if (!active) return; elements.forEach(function (rec, el) { if (el.isConnected) applyFor(el, rec); }); }
function reconcile() { if (!active) { elements.forEach(function (rec, el) { applyFor(el, rec); }); return; } requestVerdicts(onScreenPosts()); elements.forEach(function (rec, el) { if (el.isConnected) applyFor(el, rec); }); scheduleScan(); }
// Posts of tracked items still in the DOM and not yet asked about under // the current generation, nearest the viewport first — the background // judges them in that order. function onScreenPosts() { var mid = window.innerHeight / 2; var list = []; elements.forEach(function (rec, el) { if (!el.isConnected) return; var r = el.getBoundingClientRect(); var dist = Math.abs((r.top + r.bottom) / 2 - mid); [rec.main].concat(rec.nested || []).forEach(function (path) { var p = posts.get(path); if (p && askedGen.get(path) !== gen) list.push({ p: p, dist: dist }); }); }); list.sort(function (a, b) { return a.dist - b.dist; }); var seen = new Set(); return list.filter(function (x) { if (seen.has(x.p.id)) return false; seen.add(x.p.id); return true; }).map(function (x) { return x.p; }); }
/* ---- scanning ---- */
var scanScheduled = false; function scheduleScan() { if (scanScheduled) return; scanScheduled = true; // Next frame, before it paints: a freshly rendered post is hidden before // it is ever shown, and a burst of mutations costs one scan. requestAnimationFrame(function () { scanScheduled = false; scan(); }); }
function scan() { diag.scans++; elements.forEach(function (rec, el) { if (!el.isConnected) elements.delete(el); });
var anchors = document.querySelectorAll('a[href*="/post/"]'); var byItem = new Map(); for (var i = 0; i < anchors.length; i++) { if (!normalizePostPath(anchors[i].getAttribute("href"))) continue; // Cache only a sibling-confirmed item; the structural fallback can be // wrong while the list holds a single item, so it's recomputed. var item = itemOf.get(anchors[i]); if (!item || !item.isConnected || !item.contains(anchors[i])) { item = itemContainer(anchors[i]); if (item) itemOf.set(anchors[i], item); else item = nestedContainer(anchors[i]); // fallback: structural heuristic } if (!byItem.has(item)) byItem.set(item, []); byItem.get(item).push(anchors[i]); }
var unmatched = 0; var fresh = []; var nestedCount = 0; byItem.forEach(function (list, item) { var main = chooseMainAnchor(item, list); if (!main) { unmatched++; return; } var mainPath = normalizePostPath(main.getAttribute("href")); if (!mainPath) { unmatched++; return; } if (!posts.has(mainPath)) { var mp = { id: mainPath, text: cleanItemText(item), alt: itemAlts(item) }; if (indexPost(mp)) { diag.domExtracted++; fresh.push(mp); } } else if (askedGen.get(mainPath) !== gen) { fresh.push(posts.get(mainPath)); } var nested = []; for (var j = 0; j < list.length; j++) { var a = list[j]; if (a === main) continue; var np = normalizePostPath(a.getAttribute("href")); if (!np || np === mainPath || nested.indexOf(np) !== -1) continue; nested.push(np); if (!posts.has(np)) { var box = nestedContainer(a); var post = { id: np, text: cleanItemText(box), alt: itemAlts(box) }; if (indexPost(post)) { diag.domExtracted++; fresh.push(post); } } else if (askedGen.get(np) !== gen) { fresh.push(posts.get(np)); } } nestedCount += nested.length; var rec = { main: mainPath, nested: nested.length ? nested : null }; elements.set(item, rec); applyFor(item, rec); }); diag.items = elements.size; var hidden = 0; elements.forEach(function (rec, el) { if (el.classList.contains(HIDDEN_CLASS)) hidden++; }); diag.hidden = hidden; diag.unmatchedAnchors = unmatched; diag.nested = nestedCount; // Near-threshold telemetry: which tracked posts sit close to the 50% line, // under the currently active clauses' scores. var near = 0; var samples = []; elements.forEach(function (rec, el) { var d = scores.get(rec.main); if (!d) return; var s = primaryScore(settings, d); if (typeof s !== "number") return; if (Math.abs(s - 0.5) <= 0.08) { near++; if (samples.length < 6) samples.push(rec.main.slice(-16) + "=" + s); } }); diag.near50 = near; diag.near50Samples = samples.join(", "); publishDiag(); if (fresh.length) { requestVerdicts(fresh.filter(function (p, i) { return fresh.indexOf(p) === i; })); } }
var observer = new MutationObserver(scheduleScan); observer.observe(document.documentElement, { childList: true, subtree: true });})();