>moon the noob treats bans like a court case because of the stockholm syndrome xhe has after xer bvll, District Judge Brian Davis, sentenced xer to 10 years in federal prison w a lifetime of supervised release.
  • Welcome. Registering and having your account be approved may take a while since there's only one guy checking and approving accounts and guestposts
  • Boards are being moved around and added. Leave feedback in this thread and let jannies move your threads

Userscripts! Userscripts? Userscripts.

Got this hunk of junk working just a moment ago that adds 'ru-esque instant thread updates. This removes the need to refresh and check notifications as to update threads.
Fixed.
JavaScript:
// ==UserScript==
// @name         Soyjak.forum Live New Posts
// @namespace    https://greasyfork.org/
// @version      8.1.1
// @description  Polls XenForo for new thread posts every 5 seconds on the newest page only.
// @author       You
// @match        https://soyjak.forum/threads/*
// @match        https://*.soyjak.forum/threads/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function(){'use strict';

const INTERVAL=5000;
let busy=false;

const log=(...args)=>console.log('[Soyjak Live]',...args);
const getPosts=()=>[...document.querySelectorAll('.message.message--post')];

function getLastTimestamp(){
    const posts=getPosts();
    if(!posts.length)return null;
    const time=posts.at(-1).querySelector('time[data-timestamp]');
    if(!time){
        log('No timestamp found in last post:',posts.at(-1));
        return null;
    }
    const timestamp=Number(time.getAttribute('data-timestamp'));
    return Number.isFinite(timestamp)?timestamp:null;
}

function getCurrentPage(){
    const match=location.pathname.match(/\/page-(\d+)\/?$/);
    return match?Number(match[1]):1;
}

function getPageNumber(href){
    if(!href)return null;
    try{
        const url=new URL(href,location.href);
        const match=url.pathname.match(/\/page-(\d+)\/?$/);
        return match?Number(match[1]):1;
    }catch{
        return null;
    }
}

function getLastPage(){
    const current=getCurrentPage();
    const pages=[];

    document.querySelectorAll('.pageNav a[href]').forEach(link=>{
        const page=getPageNumber(link.getAttribute('href'));
        if(Number.isFinite(page))pages.push(page);
    });

    document.querySelectorAll('.pageNav-page,.pageNav-page--current').forEach(el=>{
        const text=el.textContent.trim();
        if(/^\d+$/.test(text))pages.push(Number(text));
    });

    return pages.length?Math.max(current,...pages):current;
}

function isLastPage(){
    const current=getCurrentPage();
    const last=getLastPage();
    log(`Pagination: current page ${current}, detected last page ${last}`);
    return current>=last;
}

function getNewPostsURL(after){
    const url=new URL(location.href);
    url.pathname=url.pathname.replace(/\/page-\d+\/?$/,'/');
    url.pathname=url.pathname.replace(/\/$/,'/new-posts');
    url.search='';
    url.searchParams.set('after',after);
    return url.href;
}

async function poll(){
    if(busy||!isLastPage())return;

    const after=getLastTimestamp();
    if(!after){
        log('Could not determine last post timestamp.');
        return;
    }

    busy=true;

    try{
        const url=getNewPostsURL(after);
        log('Checking:',url);

        const response=await fetch(url,{
            method:'GET',
            credentials:'same-origin',
            cache:'no-store',
            headers:{'X-Requested-With':'XMLHttpRequest'}
        });

        if(!response.ok)throw new Error(`HTTP ${response.status}`);

        const doc=new DOMParser().parseFromString(
            await response.text(),
            'text/html'
        );

        const newPosts=[
            ...doc.querySelectorAll('.message.message--post')
        ];

        if(!newPosts.length){
            log('No new posts.');
            return;
        }

        const container=document.querySelector('.block--messages');
        if(!container){
            log('Could not find message container.');
            return;
        }

        const existingIds=new Set(getPosts().map(post=>post.id));
        const freshPosts=newPosts.filter(post=>!existingIds.has(post.id));

        if(!freshPosts.length){
            log('Posts were already present.');
            return;
        }

        const atBottom=
            window.innerHeight+window.scrollY>=
            document.documentElement.scrollHeight-500;

        let insertAfter=getPosts().at(-1);

        for(const post of freshPosts){
            const imported=document.importNode(post,true);

            if(insertAfter)
                insertAfter.insertAdjacentElement('afterend',imported);
            else
                container.appendChild(imported);

            if(typeof XF!=='undefined'&&typeof XF.activate==='function')
                XF.activate(imported);

            insertAfter=imported;
        }

        log(`Inserted ${freshPosts.length} new post(s).`);

        if(atBottom)
            window.scrollTo({
                top:document.documentElement.scrollHeight,
                behavior:'smooth'
            });

    }catch(error){
        log('Error:',error);
    }finally{
        busy=false;
    }
}

if(!document.querySelector('.message.message--post'))return;

if(!isLastPage()){
    log('Not on the newest page. Live polling disabled.');
    return;
}

log('Started. Polling every 5 seconds.');
setTimeout(poll,1000);
setInterval(poll,INTERVAL);

})();
 
Last edited:
30 seconds might be to long tbh. Here is a 5 second one.
JavaScript:
// ==UserScript==
// @name         Soyjak.forum Auto-Refresh Notifications
// @namespace    https://greasyfork.org/
// @version      1.5
// @description  Refreshes the notification badge every 5 seconds on soyjak.forum without marking alerts as read
// @author       You
// @match        *://soyjak.forum/*
// @match        *://*.soyjak.forum/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const INTERVAL_MS = 5000; // 5 seconds

    function isXenForo() {
        return (
            (typeof XF !== 'undefined' && XF.ajax) ||
            (typeof XenForo !== 'undefined' && XenForo.ajax) ||
            document.documentElement?.id === 'XF' ||
            document.documentElement?.id === 'XenForo'
        );
    }

    function poll() {
        // Prefer modern XF2
        if (typeof XF !== 'undefined' && XF.ajax && XF.canonicalizeUrl) {
            // Very light endpoint that still returns the visitor counters
            // (does NOT open or mark alerts as read)
            XF.ajax(
                'GET',
                XF.canonicalizeUrl('index.php?whats-new/latest-activity'),
                {},
                null,
                {
                    global: false,        // no spinner
                    skipDefault: true,
                    error: function () {}
                }
            );
            return;
        }

        // Fallback for older XenForo
        if (typeof XenForo !== 'undefined' && XenForo.ajax) {
            XenForo.ajax(
                'index.php?misc/lightbox',
                {},
                function () {},
                {
                    global: false,
                    error: function () {}
                }
            );
        }
    }

    if (!isXenForo()) return;

    setTimeout(() => {
        poll();
        setInterval(poll, INTERVAL_MS);
    }, 2000);
})();
There's also this by Idkchud which does the same with notifications.
 
changes youtube embeds to invidious embeds like in old December
JavaScript:
// ==UserScript==
// @name         Soyjak.Forum — YouTube → Invidious
// @namespace    https://soyjak.forum/
// @version      1.0.0
// @description  Rewrites every YouTube embed to invidious.nerdvpn.de. Handles iframes, lazy-loaded embeds, nocookie, and JS-set srcs.
// @author       -
// @match        https://soyjak.forum/*
// @match        https://*.soyjak.forum/*
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    /* ------------------------------------------------------------------ */
    /*  Config                                                             */
    /* ------------------------------------------------------------------ */
    const INVIDIOUS = 'https://yt.omada.cafe';

    // Hosts we consider "YouTube". Everything here gets rewritten.
    const YT_HOSTS = new Set([
        'youtube.com',
        'www.youtube.com',
        'm.youtube.com',
        'music.youtube.com',
        'youtube-nocookie.com',
        'www.youtube-nocookie.com',
        'youtu.be',
        'www.youtu.be',
    ]);

    /* ------------------------------------------------------------------ */
    /*  URL → Invidious rewrite                                            */
    /* ------------------------------------------------------------------ */
    function rewriteUrl(raw) {
        if (!raw) return null;
        if (raw.startsWith(INVIDIOUS)) return null; // already rewritten

        let url;
        try {
            url = new URL(raw, location.href);
        } catch (e) {
            return null;
        }

        if (!YT_HOSTS.has(url.hostname.toLowerCase())) return null;

        let videoId = null;
        let start   = url.searchParams.get('start') || url.searchParams.get('t');

        // youtu.be/VIDEO_ID
        if (url.hostname.endsWith('youtu.be')) {
            videoId = url.pathname.slice(1).split('/')[0];
        }
        // /embed/VIDEO_ID, /v/VIDEO_ID, /shorts/VIDEO_ID, /live/VIDEO_ID
        else {
            const m = url.pathname.match(/^\/(embed|v|shorts|live)\/([A-Za-z0-9_-]{6,})/);
            if (m) {
                videoId = m[2];
            } else {
                // /watch?v=VIDEO_ID
                videoId = url.searchParams.get('v');
            }
        }

        if (!videoId) return null;

        const out = new URL(`${INVIDIOUS}/embed/${videoId}`);
        if (start) {
            // Normalize "1h2m3s" / "90s" / "90" → seconds
            const seconds = parseTime(start);
            if (seconds != null) out.searchParams.set('start', String(seconds));
        }
        return out.toString();
    }

    function parseTime(t) {
        if (/^\d+$/.test(t)) return parseInt(t, 10);
        const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);
        if (!m) return null;
        const [, h, mm, s] = m;
        return (parseInt(h || 0, 10) * 3600)
             + (parseInt(mm || 0, 10) * 60)
             + (parseInt(s  || 0, 10));
    }

    /* ------------------------------------------------------------------ */
    /*  Rewrite helpers                                                    */
    /* ------------------------------------------------------------------ */
    const handledIframes = new WeakSet();

    function rewriteIframe(frame) {
        if (!(frame instanceof HTMLIFrameElement)) return;

        // Handle both `src` and lazy `data-src` variants.
        for (const attr of ['src', 'data-src', 'data-lazy-src']) {
            const current = frame.getAttribute(attr);
            if (!current) continue;
            const next = rewriteUrl(current);
            if (next) frame.setAttribute(attr, next);
        }

        // Mark so we don't re-scan needlessly.
        if (!handledIframes.has(frame)) handledIframes.add(frame);
    }

    function rewriteAll(root) {
        if (!root || !root.querySelectorAll) return;
        const frames = root.querySelectorAll(
            'iframe[src], iframe[data-src], iframe[data-lazy-src]'
        );
        for (let i = 0; i < frames.length; i++) rewriteIframe(frames[i]);
    }

    /* ------------------------------------------------------------------ */
    /*  Catch every iframe the parser or JS creates                        */
    /* ------------------------------------------------------------------ */
    const mo = new MutationObserver(muts => {
        for (const m of muts) {
            if (m.type === 'childList') {
                for (const n of m.addedNodes) {
                    if (n.nodeType !== 1) continue;
                    if (n.tagName === 'IFRAME') rewriteIframe(n);
                    else rewriteAll(n);
                }
            } else if (m.type === 'attributes' && m.target.tagName === 'IFRAME') {
                rewriteIframe(m.target);
            }
        }
    });

    function startObserver() {
        if (!document.documentElement) {
            requestAnimationFrame(startObserver);
            return;
        }
        mo.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src', 'data-src', 'data-lazy-src'],
        });
        rewriteAll(document.documentElement);
    }
    startObserver();

    /* ------------------------------------------------------------------ */
    /*  Intercept the YouTube IFrame API for players created via JS        */
    /*  (XenForo's media embed occasionally uses this path)                */
    /* ------------------------------------------------------------------ */
    // Many lazy embed systems just set `iframe.src = '...'` directly.
    // The MutationObserver above already catches that via the attribute
    // filter, so no need to monkey-patch HTMLIFrameElement.src.

    /* ------------------------------------------------------------------ */
    /*  Catch <a> links that point at YouTube and are embedded inline      */
    /*  (optional — comment out if you don't want links rewritten)         */
    /* ------------------------------------------------------------------ */
    // Intentionally NOT rewriting <a href> links — this script is only
    // for *embeds*. Uncomment below if you want every YouTube link
    // navigated to Invidious too.
    /*
    function rewriteLinks(root) {
        if (!root || !root.querySelectorAll) return;
        const links = root.querySelectorAll('a[href]');
        for (const a of links) {
            const next = rewriteUrl(a.getAttribute('href'));
            if (next) a.setAttribute('href', next);
        }
    }
    */

    /* ------------------------------------------------------------------ */
    /*  Final sweep — XenForo sometimes injects embeds after `load`        */
    /* ------------------------------------------------------------------ */
    window.addEventListener('load', () => rewriteAll(document));
    setInterval(() => rewriteAll(document), 2000);
})();
 
@X3D3XD
This will make suppressing things much much more easy and not as manual
JavaScript:
// ==UserScript==
// @name         SNCapedia Shift-Click Range Select
// @namespace    https://sncapedia.org/
// @version      1.0.0
// @description  Shift-click a checkbox to tick/untick everything between it and the last checkbox you clicked. Works on page history, Special:Contributions, recent changes, logs, watchlists, etc. — handy for mass revision-delete / suppress.
// @match        https://sncapedia.org/*
// @match        https://www.sncapedia.org/*
// @match        http://sncapedia.org/*
// @match        http://www.sncapedia.org/*
// @run-at       document-idle
// @grant        none
// @noframes
// ==/UserScript==

(function () {
    'use strict';

    /* ------------------------------------------------------------------ *
     *  Containers whose checkboxes should be treated as one selectable
     *  list.  Add your own selector here if some special page is missing.
     * ------------------------------------------------------------------ */
    const CONTAINER_SELECTORS = [
        '#pagehistory',                  // Page history
        '#mw-history-compare',           // newer skins wrap the list here too
        '.mw-contributions-list',        // Special:Contributions / DeletedContributions
        '.mw-changeslist',               // Recent changes, Watchlist, Related changes
        '.mw-recentchanges',             // newer RC markup
        '#mw-watchlist-form',            // Special:Watchlist
        '.mw-logevent-loglines',         // Special:Log / suppression log
        '.mw-revisionlist',              // Special:RevisionDelete listings
        '.mw-htmlform-field-HTMLMultiSelectField'
    ].join(',');

    /* ----------------------------- helpers ---------------------------- */

    const isVisible = (el) =>
        !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);

    function findContainer(cb) {
        const known = cb.closest(CONTAINER_SELECTORS);
        if (known) return known;

        // Fallback: climb until we reach an ancestor holding 2+ checkboxes.
        let el = cb.parentElement;
        while (el && el !== document.body) {
            if (el.querySelectorAll('input[type="checkbox"]').length > 1) return el;
            el = el.parentElement;
        }
        return null;
    }

    function boxesIn(container) {
        return Array.from(container.querySelectorAll('input[type="checkbox"]'))
            .filter((cb) => !cb.disabled && isVisible(cb));
    }

    /* ------------------------------ state ----------------------------- */

    let anchor = null; // last checkbox clicked WITHOUT Shift

    /* ----------------------------- handler ---------------------------- */

    document.addEventListener('click', (event) => {
        if (event.button !== 0) return; // left click only

        let cb = event.target;

        // Direct hit on a real checkbox?
        if (!(cb instanceof HTMLInputElement && cb.type === 'checkbox')) {
            // OOUI-style checkbox: the real <input> is hidden inside a widget.
            const widget = cb instanceof Element
                ? cb.closest('.oo-ui-checkboxInputWidget')
                : null;
            cb = widget ? widget.querySelector('input[type="checkbox"]') : null;
            if (!cb) return; // labels re-dispatch a click on the input anyway
        }

        const container = findContainer(cb);
        if (!container) {
            anchor = null;
            return;
        }

        // Plain click, or Shift-click with no usable anchor → set a new anchor.
        if (!event.shiftKey || !anchor || findContainer(anchor) !== container) {
            anchor = cb;
            return;
        }

        const boxes = boxesIn(container);
        const from = boxes.indexOf(anchor);
        const to = boxes.indexOf(cb);

        if (from === -1 || to === -1) {
            anchor = cb;
            return;
        }

        const [lo, hi] = from <= to ? [from, to] : [to, from];
        const state = cb.checked; // browser already toggled the clicked box

        for (let i = lo; i <= hi; i++) {
            const box = boxes[i];
            if (box.checked === state) continue;
            box.checked = state;
            // Tell MediaWiki (and anything else listening) that it changed,
            // so "Compare selected revisions" / visibility buttons enable.
            box.dispatchEvent(new Event('input', { bubbles: true }));
            box.dispatchEvent(new Event('change', { bubbles: true }));
        }

        // The anchor deliberately stays put, so repeated shift-clicks just
        // move the far end of the range.  Uncomment the next line if you'd
        // rather each shift-click become the new anchor:
        // anchor = cb;
    }, false);
})();
 
Back
Top Bottom