The Kiwi Farms userscripts - AKA Autistically fixing the reaction system that Null broke

  • 🐕 I am attempting to get the site runnning as fast as possible. If you are experiencing slow page load times, please report it.
Status
Not open for further replies.

gagabobo1997

Soft & Chewy
True & Honest Fan
kiwifarms.net
Joined
Jan 4, 2021
Yesterday I made a thread that teaches you how to calculate your reaction score, the score that used to be displayed on user profiles that was basically a Reddit karma of sorts. One day, Null nuked both this score as well as the ability to receive notifications in your tray when you are given a sticker.
I, and many people, used the reaction notifications as a way of navigating the site. Null disagreed with this method of navigation, but after the notifications were disabled I started clicking the "reactions received" page like every 10 minutes when using the site. Having to click twice to see my reactions isn't horrible, but it triggers my autism enough to warrant building a fix for it.

So here is a little userscript I (and chatGPT) wrote that adds a new tray to the navigation menu on the site that displays a menu of your received reactions:

1736023439864.png


1736023424932.png

It's not perfect and might have some retarded bugs since I suck at javascript. If you encounter any please let me know here.

JavaScript:
// ==UserScript==
// @name         Kiwifarms Reaction Tray
// @namespace    https://kiwifarms.st/
// @version      2.6
// @description  Add a reaction recieved tray to the kiwi farms nav bar
// @author       gagabobo1997
// @match        https://kiwifarms.st/*
// @grant        none
// @sneed        chuck
// ==/UserScript==

(function() {
    'use strict';

    const REACTIONS_URL = 'https://kiwifarms.st/account/reactions';
    const ALL_REACTIONS_FINAL_PAGE_URL = 'https://kiwifarms.st/account/reactions?reaction_id=0&page=100000000';

    const style = document.createElement('style');
    style.innerHTML = `
        .p-navgroup-link--reactions {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            vertical-align: middle;
            min-width: 38px;
            min-height: 38px;
        }

        .p-navgroup-link--reactions i {
            font-size: 1em;
            font-style: normal;
            font-weight: normal;
        }

        .js-reactionsMenuBody {
            max-height: 600px;
            overflow-y: auto;
        }

        .js-reactionsMenuBody .reaction-item {
            padding: 8px 10px;
            border-bottom: 1px solid #444;
        }
        .js-reactionsMenuBody .reaction-item a {
            text-decoration: none;
        }
    `;
    document.head.appendChild(style);

    function createReactionsNav() {
        const pAccountNavGroup = document.querySelector('.p-navgroup.p-account.p-navgroup--member');
        if (!pAccountNavGroup) return;

        const link = document.createElement('a');
        link.href = '#';
        link.classList.add(
            'p-navgroup-link',
            'p-navgroup-link--iconic',
            'p-navgroup-link--reactions',
            'js-badge--reactions',
            'badgeContainer'
        );
        link.setAttribute('data-badge', '0');
        link.setAttribute('data-xf-click', 'menu');
        link.setAttribute('data-menu-pos-ref', '< .p-navgroup');
        link.setAttribute('aria-label', 'Reactions');
        link.setAttribute('aria-expanded', 'false');
        link.setAttribute('aria-haspopup', 'true');

        const icon = document.createElement('i');
        icon.textContent = 'R';
        link.appendChild(icon);

        const span = document.createElement('span');
        span.classList.add('p-navgroup-linkText');
        link.appendChild(span);

        const menuDiv = document.createElement('div');
        menuDiv.classList.add('menu', 'menu--structural', 'menu--medium');
        menuDiv.setAttribute('data-menu', 'menu');
        menuDiv.setAttribute('aria-hidden', 'true');
        menuDiv.setAttribute('data-nocache', 'true');

        const menuContent = document.createElement('div');
        menuContent.classList.add('menu-content');

        const header = document.createElement('h3');
        header.classList.add('menu-header');
        header.textContent = 'Reactions';
        menuContent.appendChild(header);

        const body = document.createElement('div');
        body.classList.add('js-reactionsMenuBody');
        body.innerHTML = '<div class="menu-row">Loading…</div>';
        menuContent.appendChild(body);

        const menuFooter = document.createElement('div');
        menuFooter.classList.add('menu-footer', 'menu-footer--split');
        menuFooter.innerHTML = `
            <div class="menu-footer-main">
                <ul class="listInline listInline--bullet">
                    <li><a href="${REACTIONS_URL}" target="_blank">Show all</a></li>
                    <li class="js-reactionTotal">Total Reactions: (loading...)</li>
                </ul>
            </div>
        `;
        menuContent.appendChild(menuFooter);
        menuDiv.appendChild(menuContent);

        const inboxLink = pAccountNavGroup.querySelector('.p-navgroup-link--conversations');
        if (inboxLink) {
            pAccountNavGroup.insertBefore(link, inboxLink);
            pAccountNavGroup.insertBefore(menuDiv, inboxLink);
        } else {
            pAccountNavGroup.appendChild(link);
            pAccountNavGroup.appendChild(menuDiv);
        }
    }

    async function calculateTotalReactions() {
        try {
            const response = await fetch(ALL_REACTIONS_FINAL_PAGE_URL);
            if (!response.ok) {
                throw new Error(`Status: ${response.status}`);
            }
            const finalUrl = response.url;
            const match = finalUrl.match(/[?&]page=(\d+)/);
            let pageNum = 1;
            if (match && match[1]) {
                pageNum = parseInt(match[1], 10);
            }

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

            const finalPageReactions = doc.querySelectorAll('.js-reactionList-0 .block-row').length;
            return ((pageNum - 1) * 20) + finalPageReactions;
        } catch (err) {
            console.error('Failed to calculate total reactions:', err);
            return null;
        }
    }

    async function fetchAndDisplayReactions() {
        const bodyContainer = document.querySelector('.js-reactionsMenuBody');
        if (!bodyContainer) return;

        try {
            const response = await fetch(REACTIONS_URL);
            if (!response.ok) {
                throw new Error(`Status: ${response.status}`);
            }
            const text = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(text, 'text/html');

            const reactionsList = doc.querySelectorAll('.js-reactionList-0 .block-row');
            const reactions = Array.from(reactionsList).map((row) => {
                const userElem = row.querySelector('.contentRow-title .username');
                const userName = userElem?.textContent.trim() || 'Unknown';
                const userProfileUrl = userElem?.getAttribute('href') || '#';

                const threadElem = row.querySelector('.contentRow-title a[href^="/threads/"]');
                const postElem = row.querySelector('.contentRow-title a[href^="/posts/"]');
                const threadTitle = threadElem?.textContent.trim() || 'Unknown thread';
                const postUrl = postElem?.getAttribute('href') || '#';

                const reactionType = row.querySelector('.reaction-text bdi')?.textContent.trim() || '??';
                const timeAttr = row.querySelector('.contentRow-minor time')?.getAttribute('title') || 'N/A';

                return {
                    userName,
                    userProfileUrl,
                    threadTitle,
                    postUrl,
                    reactionType,
                    timeAttr
                };
            });

            if (reactions.length === 0) {
                bodyContainer.innerHTML = '<div class="menu-row">No reactions found.</div>';
            } else {
                let html = '';
                reactions.forEach((r) => {
                    html += `
                        <div class="reaction-item">
                            <strong>
                                <a href="${r.userProfileUrl}" target="_blank" style="color: #ffdc00;">
                                    ${r.userName}
                                </a>
                            </strong> reacted with
                            <strong style="color: #00ff7f;">${r.reactionType}</strong>
                            <br>on your post in:
                            <a href="${r.postUrl}" target="_blank" style="color: #87ceeb;">
                                ${r.threadTitle}
                            </a>
                            <br>
                            <small style="color: #b0b0b0;">${r.timeAttr}</small>
                        </div>
                    `;
                });
                bodyContainer.innerHTML = html;
            }

            const total = await calculateTotalReactions();
            const totalEl = document.querySelector('.js-reactionTotal');
            if (totalEl) {
                totalEl.textContent = (total === null)
                    ? 'Total Reactions: (error)'
                    : `Total Reactions: ${total}`;
            }
        } catch (error) {
            console.error('Failed to fetch reactions:', error);
            bodyContainer.innerHTML = '<div class="menu-row" style="color: red;">Error loading reactions.</div>';
        }
    }

    function init() {
        createReactionsNav();
        fetchAndDisplayReactions();
        setInterval(fetchAndDisplayReactions, 60_000);
    }

    init();
})();

Features I want to add:
Automatic calculation of reaction score
Get the actual sticker images built into it
Get a better menu icon than the "R"
Add an option to put the notifications in your regular alert tray

I also want to make more general client improvements that don't have to do with stickers, but my sticker autism has been off the charts recently so I made this. I'll post any new scripts or updates I make here.
please dont ban me josh i just like my stickers
 
Last edited:
Seeing your post suddenly inspired me to go on ChatGPT and fuck around with it by making it shit out userscripts to give this site "a new look".

Here's one that is supposed to replicate the look of Kiwi Farms from 2014.
Screenshot 2025-01-04 at 15-14-35 Kiwi Farms.png

Here's another that gives Kiwi Farms "A new look".
Screenshot 2025-01-04 at 15-15-11 Kiwi Farms.png

A simple one that changes all the text to orange.
Screenshot 2025-01-04 at 15-15-57 Kiwi Farms.png

Finally, a userscript that was supposed to change all text to say "You're gae". It kinda broke the site though.
Screenshot 2025-01-04 at 15-13-58 Kiwi Farms.png
 
Seeing your post suddenly inspired me to go on ChatGPT and fuck around with it by making it shit out userscripts to give this site "a new look".

Here's one that is supposed to replicate the look of Kiwi Farms from 2014.
View attachment 6821016

Here's another that gives Kiwi Farms "A new look".
View attachment 6821017

A simple one that changes all the text to orange.
View attachment 6821018

Finally, a userscript that was supposed to change all text to say "You're gae". It kinda broke the site though.
View attachment 6821019
I'm actually working on a theming userscript too.
1736029878088.png

It lets you choose the FG color but most importantly has a rainbow mode

Here's the code, its also a WIP though:
JavaScript:
// ==UserScript==
// @name         KiwiFarms color menu
// @namespace    https://kiwifarms.st/
// @version      1.5
// @description  Change the FG color, also rainbow mode
// @author       gagabobo1997
// @match        https://kiwifarms.net/*
// @match        https://kiwifarms.st/*
// @grant        none
// @sneed        chuck
// ==/UserScript==

(function() {
  "use strict";
  const DEFAULT_COLOR = "#e1b144";
  const STORAGE_COLOR = "kfColor";
  const STORAGE_RAINBOW = "kfRainbowMode";
  let styleTag, hueTimer, hueValue = 0, rainbow = false;

  function loadColor() {
    return localStorage.getItem(STORAGE_COLOR) || DEFAULT_COLOR;
  }
  function saveColor(c) {
    localStorage.setItem(STORAGE_COLOR, c);
  }
  function loadRainbow() {
    return localStorage.getItem(STORAGE_RAINBOW) === "true";
  }
  function saveRainbow(state) {
    localStorage.setItem(STORAGE_RAINBOW, String(state));
  }
  function hexToRgb(hex) {
    let x = hex.replace("#", "");
    if (x.length === 3) x = x[0]+x[0]+x[1]+x[1]+x[2]+x[2];
    const n = parseInt(x, 16);
    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
  }
  function rgbToHex(r,g,b) {
    return "#" + [r,g,b].map(v => v.toString(16).padStart(2,"0")).join("");
  }
  function rgbToHsl(r,g,b) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r,g,b), min = Math.min(r,g,b);
    let h, s;
    const l = (max + min) / 2;
    if (max === min) {
      h = s = 0;
    } else {
      const d = max - min;
      s = l > 0.5 ? d/(2 - max - min) : d/(max + min);
      switch (max) {
        case r: h = (g - b)/d + (g < b ? 6 : 0); break;
        case g: h = (b - r)/d + 2; break;
        case b: h = (r - g)/d + 4; break;
      }
      h /= 6;
    }
    return { h, s, l };
  }
  function hslToRgb(h,s,l) {
    let r,g,b;
    if (s === 0) {
      r = g = b = l;
    } else {
      const f = (p,q,t) => {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1/6) return p + (q - p)*6*t;
        if (t < 1/2) return q;
        if (t < 2/3) return p + (q - p)*(2/3 - t)*6;
        return p;
      };
      const q = (l < 0.5) ? (l*(1+s)) : (l + s - l*s);
      const p = 2*l - q;
      r = f(p, q, h + 1/3);
      g = f(p, q, h);
      b = f(p, q, h - 1/3);
    }
    return {
      r: Math.round(r*255),
      g: Math.round(g*255),
      b: Math.round(b*255)
    };
  }
  function darken(hexColor, ratio=0.15) {
    const { r, g, b } = hexToRgb(hexColor);
    if (r == null) return hexColor;
    const { h, s, l } = rgbToHsl(r, g, b);
    const newL = Math.max(l*(1 - ratio), 0);
    const { r: rr, g: rg, b: rb } = hslToRgb(h, s, newL);
    return rgbToHex(rr, rg, rb);
  }
  function applyColor(newColor) {
    if (!styleTag) {
      styleTag = document.createElement("style");
      document.head.appendChild(styleTag);
    }
    const darker = darken(newColor, 0.15);
    styleTag.innerHTML = `
      a,
      .XenBase .block--messages .message .message-content a,
      .pageNav-page {
        color: ${newColor} !important;
      }
      .bbCodeBlock {
        border-left: 3px solid ${newColor} !important;
      }
      .block--messages .message.hb-react-threadHighlight {
        border-top: 2px solid ${newColor} !important;
      }
      .p-breadcrumbs--parent .p-breadcrumbs > li::after {
        color: ${darker} !important;
      }
      .block--category a,
      .block--category3 a,
      .block--category7 a,
      .block--category74 a,
      .block--category104 a,
      .block--category116 a {
        color: var(--link-color) !important;
      }
      .structItemContainer-group .structItem-title a {
        color: #ffffff !important;
        font-weight: 400 !important;
      }
      .structItemContainer-group .is-unread .structItem-title a {
        color: ${newColor} !important;
        font-weight: 700 !important;
      }
    `;
  }
  function tickRainbow() {
    hueValue = (hueValue + 1) % 360;
    const { r, g, b } = hslToRgb(hueValue/360, 240/255, 180/255);
    applyColor(rgbToHex(r, g, b));
  }
  function startRainbow() {
    if (hueTimer) return;
    hueTimer = setInterval(tickRainbow, 50);
  }
  function stopRainbow() {
    if (hueTimer) {
      clearInterval(hueTimer);
      hueTimer = null;
    }
  }
  function createColorMenuNav() {
    const nav = document.querySelector(".p-navgroup.p-account.p-navgroup--member");
    if (!nav) return;
    const link = document.createElement("a");
    link.href = "#";
    link.classList.add(
      "p-navgroup-link",
      "p-navgroup-link--iconic",
      "p-navgroup-link--reactions",
      "badgeContainer"
    );
    link.setAttribute("data-xf-click", "menu");
    link.setAttribute("data-menu-pos-ref", "< .p-navgroup");
    link.setAttribute("aria-label", "Color");
    link.setAttribute("aria-expanded", "false");
    link.setAttribute("aria-haspopup", "true");
    const icon = document.createElement("i");
    icon.textContent = "C";
    link.appendChild(icon);
    const span = document.createElement("span");
    span.classList.add("p-navgroup-linkText");
    link.appendChild(span);
    const menuDiv = document.createElement("div");
    menuDiv.classList.add("menu", "menu--structural", "menu--medium");
    menuDiv.setAttribute("data-menu", "menu");
    menuDiv.setAttribute("aria-hidden", "true");
    const menuContent = document.createElement("div");
    menuContent.classList.add("menu-content");
    const header = document.createElement("h3");
    header.classList.add("menu-header");
    header.textContent = "Color";
    menuContent.appendChild(header);
    const col = loadColor();
    const rainbowInit = loadRainbow();
    const body = document.createElement("div");
    body.innerHTML = `
      <div style="padding:8px;">
        <input type="color" id="colorPick" value="${col}" style="width:100%;height:30px;cursor:pointer;border:none;">
        <button id="colorReset" style="margin-top:6px;width:100%;cursor:pointer;">Reset</button>
        <label style="display:block;margin-top:6px;cursor:pointer;">
          <input type="checkbox" id="rainChk"${rainbowInit ? " checked" : ""}> Rainbow
        </label>
      </div>
    `;
    menuContent.appendChild(body);
    menuDiv.appendChild(menuContent);
    const ref = nav.querySelector(".p-navgroup-link--conversations");
    if (ref) {
      nav.insertBefore(link, ref);
      nav.insertBefore(menuDiv, ref);
    } else {
      nav.appendChild(link);
      nav.appendChild(menuDiv);
    }
    const picker = menuDiv.querySelector("#colorPick");
    const reset = menuDiv.querySelector("#colorReset");
    const rainBox = menuDiv.querySelector("#rainChk");
    picker.addEventListener("input", () => {
      const val = picker.value;
      applyColor(val);
      saveColor(val);
      if (rainbow) {
        rainbow = false;
        stopRainbow();
        rainBox.checked = false;
        saveRainbow(false);
      }
    });
    reset.addEventListener("click", () => {
      applyColor(DEFAULT_COLOR);
      saveColor(DEFAULT_COLOR);
      picker.value = DEFAULT_COLOR;
      if (rainbow) {
        rainbow = false;
        stopRainbow();
        rainBox.checked = false;
        saveRainbow(false);
      }
    });
    rainBox.addEventListener("change", () => {
      if (rainBox.checked) {
        rainbow = true;
        saveRainbow(true);
        startRainbow();
      } else {
        rainbow = false;
        saveRainbow(false);
        stopRainbow();
        applyColor(picker.value);
        saveColor(picker.value);
      }
    });
    if (rainbowInit) {
      rainbow = true;
      startRainbow();
    }
  }
  (function init() {
    createColorMenuNav();
    applyColor(loadColor());
  })();
})();


Also I think the reaction tray userscript *might* be polling too quickly (currently every 20 seconds) because I started getting a 503 error on the site. Had to hop on another IP. Sorry Null, i'll make it one minute. Though I also had like 50 KF tabs open so maybe all of them were polling at the same time.
 
Last edited:
Status
Not open for further replies.
Back