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

  • 🔧 Site instability resolved. You can report double-posts and broken attachments. For bigger issues, use the Technical Grievances thread.
    🇵🇦 Nuestro primer dominio localizado está en español en kiwifarms.pa. Our first localized domain is on Spanish on kiwifarms.pa.
  • Want to keep track of this thread?
    Accounts can bookmark posts, watch threads for updates, and jump back to where you stopped reading.
    Create account
Estado
No está abierto para más respuestas.

gagabobo1997

Soft & Chewy
True & Honest Fan
kiwifarms.net
Registrado
4 de Ene, 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
 
Última edición:
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.
Ver archivo adjunto 6821016

Here's another that gives Kiwi Farms "A new look".
Ver archivo adjunto 6821017

A simple one that changes all the text to orange.
Ver archivo adjunto 6821018

Finally, a userscript that was supposed to change all text to say "You're gae". It kinda broke the site though.
Ver archivo adjunto 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.
 
Última edición:
Congratulations OP, you played yourself. I am pretty sure this kind of thing was mentioned months and months ago.

It's like talking about your super cool fangame in public. Why would you?
 
Estado
No está abierto para más respuestas.
Atrás
Top Abajo