KiwiFarms Quick Stick

QoL sticker adder

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         KiwiFarms Quick Stick
// @description  QoL sticker adder
// @namespace    kf-quick-stick
// @author       wormpilled
// @license      MIT
// @version      1.0
// @match        https://kiwifarms.st/threads/*
// @run-at       document-end
// @grant        none
// @icon         https://kiwifarms.st/favicon.ico
// ==/UserScript==

(function () {
    'use strict';

    let activeToast = null;

    document.addEventListener('click', function (e) {
        const link = e.target.closest('a.reactionsBar-link');
        if (!link) return;

        e.preventDefault();
        e.stopImmediatePropagation();

        const url = new URL(link.href, location.origin);
        const reactionId = url.searchParams.get('reaction_id');
        if (!reactionId) {
            return;
        }

        const postMatch = link.href.match(/\/posts\/(\d+)\//);
        if (!postMatch) {
            return;
        }

        const postId = postMatch[1];

        react(postId, reactionId);
    }, true);

    // todo cleanup
    function getCsrf() {
        const meta = document.querySelector('meta[name="csrf-token"]');
        if (meta?.content) {
            return meta.content;
        }

        const input = document.querySelector('input[name="_xfToken"]');
        if (input?.value) {
            return input.value;
        }

        const scriptConfig = document.documentElement.innerHTML.match(/"csrf":"([^"]+)"/);
        if (scriptConfig) {
            return scriptConfig[1];
        }

        const m = document.cookie.match(/(?:^|;\s*)xf_csrf=([^;]+)/);
        if (m) {
            return decodeURIComponent(m[1]);
        }

        return null;
    }

    function react(postId, reactionId) {
        const csrf = getCsrf();
        if (!csrf) {
            toast('Error: No CSRF Token');
            return;
        }

        const formData = new URLSearchParams();
        formData.append('_xfResponseType', 'json');
        formData.append('_xfWithData', '1');
        formData.append('_xfRequestUri', location.pathname + location.search);
        formData.append('_xfToken', csrf);
        formData.append('reaction_id', reactionId);

        const requestUrl = new URL(`/posts/${postId}/react`, location.origin).href;

        fetch(requestUrl, {
            method: 'POST',
            credentials: 'same-origin',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'application/json'
            },
            body: formData
        })
        .then(r => {
            if (!r.ok) {
                if (r.status === 429) {
                    toast('⚠ Rate Limited! Wait before reacting again'); // todo
                    throw new Error('Rate limited');
                } else if (r.status === 403) {
                    toast('Error: Forbidden (403)');
                    throw new Error('Forbidden');
                }
            }

            return r.json();
        })
        .then(data => {
            if (data.errors && data.errors.length > 0) {
                toast(`✗ ${data.errors[0]}`);
            } else if (data?.status === 'ok' || data.hasOwnProperty('reactionId')) {
                const isRemoval = data.reactionId === null;

                if (isRemoval) {
                    toast('✗ Reaction removed');
                } else {
                    const reactionInfo = getReactionInfo(data);
                    toast(reactionInfo.name, reactionInfo.sprite);
                }

                updateReactionBar(postId, data);
            } else {
                toast('⚠ Unknown response');
            }
        })
        .catch(err => {
            toast('✗ Network error');
        });
    }

    function toast(text, spriteClass) {
        // kill toasts
        if (activeToast) {
            activeToast.remove();
            activeToast = null;
        }

        const d = document.createElement('div');

        if (spriteClass) {
            const reactionSpan = document.createElement('span');
            reactionSpan.className = spriteClass;
            reactionSpan.innerHTML = '<i aria-hidden="true"></i><img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" class="reaction-sprite js-reaction" />';
            d.appendChild(reactionSpan);
            d.appendChild(document.createTextNode(' ' + text));
        } else {
            d.textContent = text;
        }

        Object.assign(d.style, {
            position: 'fixed',
            top: '20px',
            right: '20px',
            background: '#222',
            color: '#fff',
            padding: '8px 12px',
            border: '1px solid #555',
            zIndex: 99999,
            fontSize: '13px',
            borderRadius: '4px',
            boxShadow: '0 2px 5px rgba(0,0,0,0.5)',
            display: 'flex',
            alignItems: 'center',
            gap: '4px'
        });

        document.body.appendChild(d);
        activeToast = d;

        setTimeout(() => {
            if (d.parentNode) {
                d.remove();
            }
            if (activeToast === d) {
                activeToast = null;
            }
        }, 2500);
    }

    function getReactionInfo(data) {
        if (data.html && data.html.content) {
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = data.html.content;

            const reactionSpan = tempDiv.querySelector('.reaction');
            if (reactionSpan) {
                const img = reactionSpan.querySelector('img');
                const reactionName = img ? img.getAttribute('alt') : 'Reaction';
                const spriteClass = reactionSpan.className;
                return { name: reactionName, sprite: spriteClass };
            }
        }

        return { name: `Reaction ${data.reactionId || ''}`, sprite: null };
    }

    function updateReactionBar(postId, data) {
        const postContainer = document.querySelector(`[data-content="post-${postId}"]`) ||
                              document.getElementById(`js-post-${postId}`);

        if (!postContainer) {
            return;
        }

        const reactionSummary = postContainer.querySelector('.reactPlusSummary');

        if (!reactionSummary) {
            return;
        }

        if (data.reactionList && data.reactionList.content) {
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = data.reactionList.content;
            const newSummary = tempDiv.querySelector('.reactPlusSummary');

            if (newSummary) {
                reactionSummary.innerHTML = newSummary.innerHTML;
            }
        }
    }
})();