Kick Chat Restriction Unlock

Removes rate and emote-only restrictions in KICK chat.

// ==UserScript==
// @name         Kick Chat Restriction Unlock
// @name:ja      Kickチャット制限解除
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Removes rate and emote-only restrictions in KICK chat.
// @description:ja  KICKでレート、エモートのみ制限を解除します。
// @author       XBACT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kick.com
// @match        https://kick.com/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const originalFetch = window.fetch;
    let chatroomId = null;
    let emotesModeEnabled = false;
    let initialTokenLogged = false;

    function hideWarningBoxes() {
        const warningSelectors = [
            'div[data-icon=""] > svg > path[d="M16 2C8.27 2 2 8.27 2 16C2 23.73 8.27 30 16 30C23.73 30 30 23.73 30 16C30 8.27 23.73 2 16 2ZM18 24.76H14V14.09H18V24.76ZM18 11.09H14V7.23H18V11.09Z"]',
            '.fill-danger-lower',
            '[class*="danger"]',
            '[class*="warning"]',
            'div[data-content=""] > div[data-title=""]',
            '[data-testid*="mod-action"]',
            '[data-testid*="warning"]',
            '.chat-notification.warning'
        ];

        warningSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(el => {
                el.closest('div')?.remove();
            });
        });

        if (!document.getElementById('kick-enhancer-hide-warnings')) {
            const style = document.createElement('style');
            style.id = 'kick-enhancer-hide-warnings';
            style.textContent = `
                div[data-icon=""] svg.fill-danger-lower { display: none !important; }
                div[data-content=""] { display: none !important; }
                .fill-danger-lower { display: none !important; }
                [class*="mod-action"]:has(svg path[d*="M16 2C8.27 2"]) { display: none !important; }
                .chat-warning, .chat-mod-box { display: none !important; }
            `;
            document.head.appendChild(style);
        }

        if (!window.kickWarningObserver) {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeType === 1) {
                            if (node.matches && (node.matches('div[data-icon=""]') || node.querySelector('svg.fill-danger-lower'))) {
                                node.style.display = 'none';
                            }
                        }
                    });
                });
            });
            observer.observe(document.body, { childList: true, subtree: true });
            window.kickWarningObserver = observer;
        }
    }

    window.fetch = async function(...args) {
        let [resource, config] = args;
        let url = '';

        try {
            url = typeof resource === 'string' ? resource : resource.url;
        } catch (e) {
            return originalFetch.apply(this, args);
        }

        if (!url.startsWith('https://kick.com/api/v2/')) {
            return originalFetch.apply(this, args);
        }

        let requestHeaders = {};
        if (config && config.headers) {
            if (config.headers instanceof Headers) {
                config.headers.forEach((v, k) => { requestHeaders[k.toLowerCase()] = v; });
            } else {
                for (const k in config.headers) { requestHeaders[k.toLowerCase()] = config.headers[k]; }
            }
        } else if (resource instanceof Request && resource.headers) {
            resource.headers.forEach((v, k) => { requestHeaders[k.toLowerCase()] = v; });
        }

        const authHeaderValue = requestHeaders['authorization'];
        if (authHeaderValue && typeof authHeaderValue === 'string' && authHeaderValue.startsWith('Bearer ')) {
            const token = authHeaderValue.substring(7);
            if (token && token !== localStorage.getItem('bearerToken')) {
                localStorage.setItem('bearerToken', token);
                window.bearerTokenGlobal = token;
                initialTokenLogged = true;
            }
        }

        if (url.includes('/api/v2/channels/') && url.includes('/chatroom')) {
            const response = await originalFetch.apply(this, args);
            const clonedResponse = response.clone();
            try {
                const data = await clonedResponse.json();
                const newId = data.id;
                if (newId && newId !== chatroomId) {
                    chatroomId = newId;
                }
                const newEmotesMode = data.emotes_mode?.enabled;
                if (typeof newEmotesMode !== 'undefined' && newEmotesMode !== emotesModeEnabled) {
                    emotesModeEnabled = newEmotesMode;
                }
            } catch (e) {}
            return response;
        }

        return originalFetch.apply(this, args);
    };

    window.bearerTokenGlobal = localStorage.getItem('bearerToken') || null;

    function setupChatInterceptor() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initializeChat);
        } else {
            initializeChat();
        }
    }

    function initializeChat() {
        const findChatElements = () => {
            const chatInputWrapper = document.getElementById('chat-input-wrapper');
            const sendButton = document.getElementById('send-message-button');
            const editorInput = document.querySelector('.editor-input[data-lexical-editor="true"]');

            if (!chatInputWrapper || !sendButton || !editorInput) {
                setTimeout(findChatElements, 1000);
                return;
            }

            function clearEditorInput(editor) {
                editor.focus();
                const range = document.createRange();
                range.selectNodeContents(editor);
                const selection = window.getSelection();
                selection.removeAllRanges();
                selection.addRange(range);
                editor.dispatchEvent(new KeyboardEvent('keydown', {
                    key: 'a',
                    code: 'KeyA',
                    ctrlKey: true,
                    bubbles: true,
                    cancelable: true
                }));
                setTimeout(() => {
                    const textLength = editor.textContent.length;
                    for (let i = 0; i < textLength + 5; i++) {
                        editor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', code: 'Backspace', keyCode: 8, bubbles: true, cancelable: true }));
                        editor.dispatchEvent(new KeyboardEvent('keyup', { key: 'Backspace', code: 'Backspace', keyCode: 8, bubbles: true, cancelable: true }));
                    }
                    setTimeout(() => {
                        const p = editor.querySelector('p.editor-paragraph');
                        if (p) {
                            p.innerHTML = '<br>';
                        }
                        editor.focus();
                    }, 100);
                }, 100);
            }

            sendButton.addEventListener('click', async (event) => {
                event.preventDefault();
                event.stopPropagation();
                const message = editorInput.textContent.trim();
                if (message) {
                    clearEditorInput(editorInput);
                    await sendMessage(message);
                }
            });

            editorInput.addEventListener('keydown', async (event) => {
                if (event.key === 'Enter' && !event.shiftKey) {
                    event.preventDefault();
                    const message = editorInput.textContent.trim();
                    if (message) {
                        clearEditorInput(editorInput);
                        await sendMessage(message);
                    }
                }
            });
        };
        findChatElements();
    }

    async function sendMessage(message) {
        if (!window.bearerTokenGlobal || !chatroomId) return;
        const finalMessage = emotesModeEnabled ? `[emote:37230:POLICE] ${message} [emote:37230:POLICE]` : message;
        const url = `https://kick.com/api/v2/messages/send/${chatroomId}`;
        const headers = {
            "accept": "application/json",
            "content-type": "application/json",
            "Authorization": `Bearer ${window.bearerTokenGlobal}`,
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
        };
        const data = {
            "content": finalMessage,
            "type": "message",
            "message_ref": String(Date.now())
        };
        try {
            const response = await originalFetch(url, {
                method: 'POST',
                headers: headers,
                body: JSON.stringify(data)
            });
            if (!response.ok) {
                if (response.status === 401 || response.status === 403) {
                    localStorage.removeItem('bearerToken');
                    window.bearerTokenGlobal = null;
                    initialTokenLogged = false;
                }
            }
        } catch {}
    }

    setupChatInterceptor();

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            hideWarningBoxes();
            setInterval(hideWarningBoxes, 2000);
        });
    } else {
        hideWarningBoxes();
        setInterval(hideWarningBoxes, 2000);
    }

})();