Drawaria No Censor Words

Bypass Drawaria.online censorship by converting text to bold Unicode characters.

// ==UserScript==
// @name         Drawaria No Censor Words
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Bypass Drawaria.online censorship by converting text to bold Unicode characters.
// @author       YouTubeDrawaria
// @match        https://drawaria.online/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=drawaria.online
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // A simple utility to create DOM elements, ensuring the script is truly standalone.
    const domMake = {
        Tree: function(tagName, attributes = {}, children = []) {
            const element = document.createElement(tagName);
            for (const key in attributes) {
                if (attributes.hasOwnProperty(key)) {
                    if (key === 'style' && typeof attributes[key] === 'string') {
                        element.style.cssText = attributes[key];
                    } else if (key in element && tagName !== 'input') { // Avoid setting value attribute as property for inputs
                        element[key] = attributes[key];
                    } else {
                        element.setAttribute(key, attributes[key]);
                    }
                }
            }
            children.forEach(child => {
                if (typeof child === 'string') {
                    element.appendChild(document.createTextNode(child));
                } else if (child instanceof Node) {
                    element.appendChild(child);
                }
            });
            return element;
        },
        Button: function(text, attributes = {}) {
            const button = document.createElement('button');
            button.textContent = text;
            for (const key in attributes) {
                if (attributes.hasOwnProperty(key)) {
                    if (key === 'style' && typeof attributes[key] === 'string') {
                        button.style.cssText = attributes[key];
                    } else {
                        button.setAttribute(key, attributes[key]);
                    }
                }
            }
            return button;
        }
    };


    // --- Character map for Bold Text ---
    const BOLD_TEXT_MAP = {
        'A': '𝗔', 'B': '𝗕', 'C': '𝗖', 'D': '𝗗', 'E': '𝗘', 'F': '𝗙', 'G': '𝗚', 'H': '𝗛', 'I': '𝗜', 'J': '𝗝', 'K': '𝗞', 'L': '𝗟', 'M': '𝗠', 'N': '𝗡', 'O': '𝗢', 'P': '𝗣', 'Q': '𝗤', 'R': '𝗥', 'S': '𝗦', 'T': '𝗧', 'U': '𝗨', 'V': '𝗩', 'W': '𝗪', 'X': '𝗫', 'Y': '𝗬', 'Z': '𝗭', 'a': '𝗮', 'b': '𝗯', 'c': '𝗰', 'd': '𝗱', 'e': '𝗲', 'f': '𝗳', 'g': '𝗴', 'h': '𝗵', 'i': '𝗶', 'j': '𝗷', 'k': '𝗸', 'l': '𝗹', 'm': '𝗺', 'n': '𝗻', 'o': '𝗼', 'p': '𝗽', 'q': '𝗾', 'r': '𝗿', 's': '𝘀', 't': '𝘁', 'u': '𝘂', 'v': '𝘃', 'w': '𝘄', 'x': '𝘅', 'y': '𝘆', 'z': '𝘇', '0': '𝟬', '1': '𝟭', '2': '𝟮', '3': '𝟯', '4': '𝟰', '5': '𝟱', '6': '𝟲', '7': '𝟳', '8': '𝟴', '9': '𝟵'
    };

    class NoCensorWords {
        #panel = null;
        #toggleButton = null;
        #isCensorBypassActive = false;
        #chatInputObserver = null;
        #currentChatInput = null; // Reference to the currently active chat input element
        #inputListener = null; // Store the listener reference for proper removal
        #observerTarget = null;
        #_attachInputDebounceTimer = null; // Private property for the debounce timer

        constructor() {
            console.log("NoCensorWords: Initializing...");
            this.#addStyles();
            this.#createPanel();
            this.#setupChatInputMonitoring();
            console.log("NoCensorWords: Initialization complete.");
        }

        #addStyles() {
            const style = document.createElement('style');
            style.textContent = `
                .no-censor-button {
                    background: #f0f0f0; /* Light background */
                    color: #333; /* Dark text */
                    border: 1px solid #ccc;
                    border-radius: 5px;
                    padding: 8px 12px;
                    font-size: 1em;
                    cursor: pointer;
                    transition: background 0.16s ease-in-out, color 0.16s ease-in-out;
                    width: 100%; /* Make button fill panel width */
                    box-sizing: border-box; /* Include padding and border in width */
                }
                .no-censor-button:hover {
                    background: #e0e0e0; /* Slightly darker on hover */
                }
                .no-censor-button.active {
                    background: #4CAF50; /* Green for active */
                    border-color: #4CAF50;
                    color: #fff; /* White text for active */
                    font-weight: bold;
                }
                .no-censor-button.active:hover {
                    background: #5cb85c;
                }
                .no-censor-panel {
                    position: fixed;
                    top: 100px; /* Default position */
                    right: 20px;
                    width: 220px; /* Adjusted width to be more compact */
                    background: #ffffff; /* White background for the panel */
                    border: 1px solid #ddd;
                    border-radius: 8px;
                    box-shadow: 0 4px 12px rgba(0,0,0,0.2);
                    z-index: 100000; /* High z-index to be on top */
                    color: #333; /* Default text color for panel content */
                    font-family: Arial, sans-serif;
                    padding: 10px;
                    box-sizing: border-box;
                    user-select: none; /* Prevent text selection on the panel itself */
                }
                .no-censor-panel-header {
                    cursor: grab;
                    padding: 5px;
                    background: #f0f0f0; /* Light header background */
                    border-bottom: 1px solid #e0e0e0;
                    margin: -10px -10px 10px -10px; /* Negative margin to pull it to edges */
                    border-top-left-radius: 8px;
                    border-top-right-radius: 8px;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    user-select: none; /* Prevent text selection on the header */
                }
                .no-censor-panel-header span {
                    font-weight: bold;
                    font-size: 1.1em;
                    margin-left: 10px;
                    color: #333; /* Dark text for title */
                }
                .no-censor-panel-close-btn {
                    background: none;
                    border: none;
                    color: #666; /* Darker close button color */
                    font-size: 1.5em;
                    cursor: pointer;
                    padding: 0 5px;
                }
                .no-censor-panel-close-btn:hover {
                    color: #ff0000;
                }
            `;
            document.head.appendChild(style);
            console.log("NoCensorWords: Styles added.");
        }

        #createPanel() {
            this.#panel = domMake.Tree('div', { class: 'no-censor-panel' });

            const header = domMake.Tree('div', { class: 'no-censor-panel-header' });
            const title = domMake.Tree('span', {}, ["No Censor Words"]);
            const closeButton = domMake.Button('×', { class: 'no-censor-panel-close-btn' });
            closeButton.onclick = () => {
                this.#panel.remove();
                if (this.#chatInputObserver) {
                    this.#chatInputObserver.disconnect();
                    console.log("NoCensorWords: MutationObserver disconnected on panel close.");
                }
                if (this.#currentChatInput && this.#inputListener) {
                    this.#currentChatInput.removeEventListener('input', this.#inputListener);
                    console.log("NoCensorWords: Chat input listener removed on panel close.");
                }
                console.log("NoCensorWords: Panel removed.");
            };

            // FIX: Use standard appendChild instead of custom appendAll
            header.appendChild(title);
            header.appendChild(closeButton);

            this.#panel.appendChild(header);

            this.#toggleButton = domMake.Button('Activar Bypass', { class: 'no-censor-button' });
            this.#toggleButton.onclick = () => this.#toggleCensorBypass();
            this.#panel.appendChild(this.#toggleButton);

            document.body.appendChild(this.#panel);
            this.#makeDraggable(this.#panel, header);
            console.log("NoCensorWords: Panel created and appended to body.");
        }

        #makeDraggable(element, handle) {
            let offsetX, offsetY;
            let isDragging = false;

            const startDrag = (e) => {
                // Ensure event target is the handle itself, not a child of the handle
                if (e.target !== handle && e.target !== handle.firstElementChild) {
                    // Check if the target is the close button, don't drag if so
                    if (e.target.classList.contains('no-censor-panel-close-btn')) {
                        return;
                    }
                }

                e.preventDefault(); // Prevent default browser drag behavior

                const rect = element.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;

                isDragging = true;
                handle.style.cursor = 'grabbing';
                document.body.style.userSelect = 'none'; // Prevent text selection on body

                document.addEventListener('mousemove', doDrag);
                document.addEventListener('mouseup', stopDrag);
            };

            const doDrag = (e) => {
                if (!isDragging) return;
                e.preventDefault();

                let newLeft = e.clientX - offsetX;
                let newTop = e.clientY - offsetY;

                // Clamp to viewport boundaries
                newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - element.offsetWidth));
                newTop = Math.max(0, Math.min(newTop, window.innerHeight - element.offsetHeight));

                element.style.left = `${newLeft}px`;
                element.style.top = `${newTop}px`;
            };

            const stopDrag = () => {
                isDragging = false;
                handle.style.cursor = 'grab';
                document.body.style.userSelect = 'auto'; // Restore text selection

                document.removeEventListener('mousemove', doDrag);
                document.removeEventListener('mouseup', stopDrag);
            };

            handle.addEventListener('mousedown', startDrag);
            handle.style.cursor = 'grab'; // Set default cursor for handle
            console.log("NoCensorWords: Panel made draggable.");
        }

        #toggleCensorBypass() {
            this.#isCensorBypassActive = !this.#isCensorBypassActive;
            if (this.#isCensorBypassActive) {
                this.#toggleButton.textContent = 'Desactivar Bypass';
                this.#toggleButton.classList.add('active');
                this.#applyConversionToCurrentInput(); // Apply to existing text if any
                console.log("NoCensorWords: Censura bypass ACTIVADO.");
            } else {
                this.#toggleButton.textContent = 'Activar Bypass';
                this.#toggleButton.classList.remove('active');
                // Reverting text is complex, simply stop future conversions.
                // The user would need to retype if they want original characters.
                console.log("NoCensorWords: Censura bypass DESACTIVADO.");
            }
        }

        #getChatInput() {
            // This attempts to get the chat input. It targets the id used by Drawaria,
            // which could be the original input or a textarea replaced by other scripts.
            return document.querySelector('#chatbox_textinput');
        }

        #setupChatInputMonitoring() {
            // Find a stable parent element to observe for chat input changes
            this.#observerTarget = document.querySelector('#chatattop') || document.body;
            if (!this.#observerTarget) {
                console.error("NoCensorWords: Could not find chat container or body to observe for input changes.");
                return;
            }

            this.#chatInputObserver = new MutationObserver(() => {
                // Debounce the attachment to avoid multiple calls on rapid DOM changes
                clearTimeout(this.#_attachInputDebounceTimer); // Use the private property
                this.#_attachInputDebounceTimer = setTimeout(() => { // Assign to private property
                    this.#attachInputListener();
                }, 100); // Small delay to let DOM settle
            });

            // Observe for changes in the chat container or body for the input element
            this.#chatInputObserver.observe(this.#observerTarget, { childList: true, subtree: true, attributes: false });
            console.log("NoCensorWords: MutationObserver set up on", this.#observerTarget === document.body ? "body" : "#chatattop");

            // Initial attachment in case input is already present when script loads
            this.#attachInputListener();
        }

        #attachInputListener() {
            const newChatInput = this.#getChatInput();

            if (newChatInput && newChatInput !== this.#currentChatInput) {
                // Remove old listener if it exists and is different from new input
                if (this.#currentChatInput && this.#inputListener) {
                    this.#currentChatInput.removeEventListener('input', this.#inputListener);
                    console.log("NoCensorWords: Removed old chat input listener.");
                }

                this.#currentChatInput = newChatInput;
                this.#inputListener = (event) => this.#handleChatInput(event);
                this.#currentChatInput.addEventListener('input', this.#inputListener);
                console.log("NoCensorWords: Attached new chat input listener to:", this.#currentChatInput);
                this.#applyConversionToCurrentInput(); // Apply conversion immediately upon attaching
            } else if (!newChatInput && this.#currentChatInput) {
                // Input disappeared
                this.#currentChatInput.removeEventListener('input', this.#inputListener);
                this.#currentChatInput = null;
                this.#inputListener = null;
                console.log("NoCensorWords: Chat input disappeared, listener removed.");
            } else if (newChatInput === this.#currentChatInput) {
                // console.log("NoCensorWords: Chat input is the same, no re-attachment needed.");
            } else {
                // console.log("NoCensorWords: Chat input not found yet.");
            }
        }

        #handleChatInput(event) {
            if (!this.#isCensorBypassActive || !this.#currentChatInput) {
                return;
            }

            const currentText = this.#currentChatInput.value;
            const originalSelectionStart = this.#currentChatInput.selectionStart;
            const originalSelectionEnd = this.#currentChatInput.selectionEnd;

            let convertedText = '';
            let charOffset = 0; // Tracks the change in length due to conversion

            for (let i = 0; i < currentText.length; i++) {
                const originalChar = currentText[i];
                // Convert uppercase and lowercase letters, and digits
                const convertedChar = BOLD_TEXT_MAP[originalChar] || BOLD_TEXT_MAP[originalChar.toUpperCase()] || originalChar;

                convertedText += convertedChar;

                // Adjust charOffset if character length changes
                if (convertedChar.length !== originalChar.length) {
                    charOffset += (convertedChar.length - originalChar.length);
                }
            }

            // Only update if conversion actually changed the text to prevent infinite loops
            if (this.#currentChatInput.value !== convertedText) {
                // FIX: Directly assign the value. This is the safest way to update.
                this.#currentChatInput.value = convertedText;

                // Restore cursor and selection, adjusting for length changes
                this.#currentChatInput.selectionStart = originalSelectionStart + charOffset;
                this.#currentChatInput.selectionEnd = originalSelectionEnd + charOffset;

                // Optionally, dispatch an input event manually after setting value
                // if framework relies on it, though direct .value usually doesn't trigger 'input'
                // and we already have a wrapper dispatching in #applyConversionToCurrentInput.
                // No need to dispatch here unless required by a specific framework.
            }
        }

        // Applies conversion to the current input value immediately when toggled on
        #applyConversionToCurrentInput() {
            if (this.#currentChatInput && this.#isCensorBypassActive) {
                // Dispatch a synthetic 'input' event to trigger #handleChatInput
                // This is crucial because directly setting .value doesn't fire 'input'.
                const inputEvent = new Event('input', { bubbles: true });
                this.#currentChatInput.dispatchEvent(inputEvent);
            }
        }
    }

    // Initialize the module
    function initNoCensorWordsScript() {
        console.log("NoCensorWords: DOMContentLoaded or script executed.");
        // Add a small delay to ensure the main Drawaria chat input is ready,
        // especially if other scripts (like AdvancedChatEnhancements) are also modifying it.
        setTimeout(() => {
            new NoCensorWords();
        }, 1500); // 1.5 seconds delay
    }

    // Ensure the script runs after the DOM is fully loaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initNoCensorWordsScript);
    } else {
        initNoCensorWordsScript();
    }

})();