Discord DM Sender with Input Box and Toggle

Send DM messages via Discord API with toggle visibility, customizable button names, and instructions. Supports "Shift + Enter" for new line and "Enter" for sending messages.

// ==UserScript==
// @name         Discord DM Sender with Input Box and Toggle
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Send DM messages via Discord API with toggle visibility, customizable button names, and instructions. Supports "Shift + Enter" for new line and "Enter" for sending messages.
// @author       Your Name
// @match        https://discord.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license      You can modify as long as you credit me
// ==/UserScript==

(function() {
    'use strict';

    let isBoxVisible = GM_getValue('isBoxVisible', true);
    let isTokenVisible = GM_getValue('isTokenVisible', true);
    let areChannelsVisible = GM_getValue('areChannelsVisible', true);
    let channelId = '';

    const initialWidth = '280px';
    const initialHeight = '500px';

    const container = document.createElement('div');
    container.style.position = 'fixed';
    container.style.bottom = '10px';
    container.style.left = '10px';
    container.style.backgroundColor = '#2f3136';
    container.style.color = '#ffffff';
    container.style.padding = '10px';
    container.style.borderRadius = '5px';
    container.style.zIndex = '1000';
    container.style.width = initialWidth;
    container.style.height = initialHeight;
    container.style.maxHeight = '90vh';
    container.style.overflow = 'auto';
    container.style.display = isBoxVisible ? 'block' : 'none';
    document.body.appendChild(container);

    makeElementDraggable(container);

    const hideTokenButton = document.createElement('button');
    hideTokenButton.innerText = isTokenVisible ? 'Hide Token' : 'View Token';
    hideTokenButton.style.marginBottom = '10px';
    hideTokenButton.style.width = '100%';
    hideTokenButton.style.backgroundColor = '#575757';
    hideTokenButton.style.color = '#ffffff';
    hideTokenButton.style.border = 'none';
    hideTokenButton.style.borderRadius = '3px';
    hideTokenButton.style.cursor = 'pointer';
    hideTokenButton.addEventListener('click', () => {
        isTokenVisible = !isTokenVisible;
        GM_setValue('isTokenVisible', isTokenVisible);
        tokenBox.style.display = isTokenVisible ? 'block' : 'none';
        hideTokenButton.innerText = isTokenVisible ? 'Hide Token' : 'View Token';
    });
    container.appendChild(hideTokenButton);

    const tokenBox = document.createElement('textarea');
    tokenBox.placeholder = 'Enter your token';
    tokenBox.style.width = '100%';
    tokenBox.style.height = '40px';
    tokenBox.style.resize = 'none';
    tokenBox.style.backgroundColor = '#000000';
    tokenBox.style.color = '#00FF00';
    tokenBox.style.display = isTokenVisible ? 'block' : 'none';
    tokenBox.value = GM_getValue('tokenBoxValue', '');
    tokenBox.addEventListener('input', () => {
        GM_setValue('tokenBoxValue', tokenBox.value);
    });
    container.appendChild(tokenBox);

    const toggleChannelsButton = document.createElement('button');
    toggleChannelsButton.innerText = areChannelsVisible ? 'Hide Channel IDs' : 'View Channel IDs';
    toggleChannelsButton.style.marginTop = '10px';
    toggleChannelsButton.style.width = '100%';
    toggleChannelsButton.style.backgroundColor = '#575757';
    toggleChannelsButton.style.color = '#ffffff';
    toggleChannelsButton.style.border = 'none';
    toggleChannelsButton.style.borderRadius = '3px';
    toggleChannelsButton.style.cursor = 'pointer';
    toggleChannelsButton.addEventListener('click', () => {
        areChannelsVisible = !areChannelsVisible;
        GM_setValue('areChannelsVisible', areChannelsVisible);
        channelBoxes.forEach((channelBox) => {
            channelBox.style.display = areChannelsVisible ? 'block' : 'none';
        });
        toggleChannelsButton.innerText = areChannelsVisible ? 'Hide Channel IDs' : 'View Channel IDs';
    });
    container.appendChild(toggleChannelsButton);

    const channelBoxes = [];
    const channelBoxPlaceholders = [
        '荒らし雑談用匿名BOTのDMチャンネルIDを入力',
        '情勢雑談用BOTの匿名BOTのDMチャンネルIDを入力',
        '依頼支部用匿名BOTのDMチャンネルIDを入力'
    ];

    channelBoxPlaceholders.forEach((placeholder, index) => {
        const channelBox = document.createElement('textarea');
        channelBox.placeholder = placeholder;
        channelBox.style.width = '100%';
        channelBox.style.height = '40px';
        channelBox.style.resize = 'none';
        channelBox.style.backgroundColor = '#000000';
        channelBox.style.color = '#00FF00';
        channelBox.style.display = areChannelsVisible ? 'block' : 'none';
        channelBox.value = GM_getValue(`channelBox${index + 1}Value`, '');
        channelBox.addEventListener('input', () => {
            GM_setValue(`channelBox${index + 1}Value`, channelBox.value);
        });
        channelBoxes.push(channelBox);
        container.appendChild(channelBox);
    });

    const inputBox = document.createElement('textarea');
    inputBox.placeholder = 'Enter message (⚠️:初回はcaptchaにかかる事がある為、初回送信時はbotに1回手動でDMを送ってから使用してください。DMチャンネルIDはBOT IDではありません)';
    inputBox.style.width = '100%';
    inputBox.style.height = '100px';
    inputBox.style.resize = 'none';
    inputBox.style.backgroundColor = '#000000';
    inputBox.style.color = '#00FF00';
    inputBox.style.marginTop = '10px';
    inputBox.value = GM_getValue('inputBoxValue', '');
    inputBox.addEventListener('input', () => {
        GM_setValue('inputBoxValue', inputBox.value);
    });
    inputBox.addEventListener('keydown', (event) => {
        if (event.key === 'Enter' && !event.shiftKey) {
            event.preventDefault(); 
            
            const token = tokenBox.value.trim();
            if (!token) {
                alert('Token is required');
                return;
            }

            const message = inputBox.value.trim();
            if (!message) {
                alert('Message cannot be empty');
                return;
            }

            sendMessage(channelId, message, token).then((success) => {
                if (success) {
                    inputBox.value = '';
                    GM_setValue('inputBoxValue', '');
                } else {
                    alert('Failed to send message');
                }
            });
        }
    });
    container.appendChild(inputBox);

    const selectChannelsLabel = document.createElement('div');
    selectChannelsLabel.innerText = 'Select channels';
    selectChannelsLabel.style.marginTop = '10px';
    selectChannelsLabel.style.marginBottom = '5px';
    selectChannelsLabel.style.fontSize = '14px';
    selectChannelsLabel.style.textAlign = 'left';
    container.appendChild(selectChannelsLabel);

    const buttonContainer = document.createElement('div');
    buttonContainer.style.display = 'flex';
    buttonContainer.style.justifyContent = 'space-between';

    const buttonNames = ['荒らし雑談', '情勢雑談', '依頼支部'];

    channelBoxes.forEach((channelBox, index) => {
        const channelButton = document.createElement('button');
        channelButton.innerText = buttonNames[index];
        channelButton.style.width = '30%';
        channelButton.style.backgroundColor = '#575757';
        channelButton.style.color = '#ffffff';
        channelButton.style.border = 'none';
        channelButton.style.borderRadius = '3px';
        channelButton.style.cursor = 'pointer';
        channelButton.addEventListener('click', () => {
            channelId = channelBox.value.trim();
            updateButtonStyles(channelButton);
        });
        buttonContainer.appendChild(channelButton);
    });

    function updateButtonStyles(activeButton) {
        Array.from(buttonContainer.children).forEach((button) => {
            button.style.backgroundColor = button === activeButton ? '#047500' : '#575757';
        });
    }

    container.appendChild(buttonContainer);

    const sendButton = document.createElement('button');
    sendButton.innerText = 'Send DM';
    sendButton.style.marginTop = '10px';
    sendButton.style.width = '100%';
    sendButton.style.backgroundColor = '#575757';
    sendButton.style.color = '#ffffff';
    sendButton.style.border = 'none';
    sendButton.style.borderRadius = '3px';
    sendButton.style.cursor = 'pointer';
    sendButton.addEventListener('click', async () => {
        const token = tokenBox.value.trim();
        if (!token) {
            alert('Token is required');
            return;
        }

        const message = inputBox.value.trim();
        if (!message) {
            alert('Message cannot be empty');
            return;
        }

        const success = await sendMessage(channelId, message, token);
        if (success) {
            inputBox.value = '';
            GM_setValue('inputBoxValue', '');
        } else {
            alert('Failed to send message');
        }
    });
    container.appendChild(sendButton);

    const toggleImage = document.createElement('img');
    toggleImage.src = 'https://i.imgur.com/FL6WD8a.png';
    toggleImage.style.position = 'fixed';
    toggleImage.style.width = '30px';
    toggleImage.style.height = '30px';
    toggleImage.style.cursor = 'pointer';
    toggleImage.style.zIndex = '1001';
    toggleImage.style.left = '75px';
    toggleImage.style.bottom = '222px';
    document.body.appendChild(toggleImage);

    toggleImage.addEventListener('click', () => {
        isBoxVisible = !isBoxVisible;
        GM_setValue('isBoxVisible', isBoxVisible);
        container.style.display = isBoxVisible ? 'block' : 'none';
    });

    function makeElementDraggable(el) {
        el.onmousedown = function(event) {
            if (event.target.tagName === 'TEXTAREA') return;

            event.preventDefault();

            let shiftX = event.clientX - el.getBoundingClientRect().left;
            let shiftY = event.clientY - el.getBoundingClientRect().top;

            function moveAt(pageX, pageY) {
                const newLeft = Math.min(Math.max(0, pageX - shiftX), window.innerWidth - el.offsetWidth);
                const newTop = Math.min(Math.max(0, pageY - shiftY), window.innerHeight - el.offsetHeight);

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

            function stopDragging() {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', stopDragging);
            }

            function onMouseMove(event) {
                moveAt(event.pageX, event.pageY);
            }

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', stopDragging);
        };

        el.ondragstart = function() {
            return false;
        };
    }

    async function sendMessage(channelId, message, token) {
        const nonce = generateNonce();
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: `https://discord.com/api/v9/channels/${channelId}/messages`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': token
                },
                data: JSON.stringify({
                    content: message,
                    flags: 0,
                    nonce: nonce,
                    tts: false
                }),
                onload: (response) => {
                    resolve(response.status === 200);
                },
                onerror: () => resolve(false)
            });
        });
    }

    function generateNonce() {
        const now = Date.now();
        return `${now}${Math.floor(Math.random() * 1000)}`;
    }
})();