Twitter Like and Send to Discord

Send tweets to Discord on like and with custom button, using vxtwitter.com links

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Twitter Like and Send to Discord
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Send tweets to Discord on like and with custom button, using vxtwitter.com links
// @match        https://twitter.com/*
// @match        https://x.com/*
// @author       dr.bobo0
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @license      MIT
// @connect      discord.com
// ==/UserScript==

(function() {
    'use strict';

    let config = {
        discordWebhookUrl: GM_getValue('discordWebhookUrl', ''),
        autoSendEnabled: GM_getValue('autoSendEnabled', true)
    };
    let sentTweets = new Set(JSON.parse(GM_getValue('sentTweets', '[]')));

    const baseUrl = 'https://vxtwitter.com';
    const defaultSVG = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard" viewBox="0 0 24 24" stroke-width="2" stroke="#71767C" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24V24H0z" fill="none"/><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><path d="M9 3m0 2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v0a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2z"/></svg>';
    const copiedSVG = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clipboard-check" viewBox="0 0 24 24" stroke-width="2" stroke="#00abfb" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24V24H0z" fill="none"/><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><path d="M9 3m0 2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v0a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2z"/><path d="M9 14l2 2 4-4"/></svg>';

    GM_addStyle(`
        .modal {
            display: none;
            position: fixed;
            z-index: 10000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.4);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }
        .modal-content {
            background-color: #15202B;
            color: #ffffff;
            margin: 15% auto;
            padding: 20px;
            border: 1px solid #38444D;
            border-radius: 15px;
            width: 80%;
            max-width: 500px;
        }
        .close {
            color: #8899A6;
            float: right;
            font-size: 28px;
            font-weight: bold;
            cursor: pointer;
        }
        .close:hover,
        .close:focus {
            color: #1DA1F2;
            text-decoration: none;
            cursor: pointer;
        }
        .modal h2 {
            color: #ffffff;
            margin-bottom: 20px;
        }
        .modal label {
            display: block;
            margin-bottom: 10px;
            color: #8899A6;
        }
        .modal input[type="text"] {
            width: 100%;
            padding: 8px;
            margin-bottom: 20px;
            border: 1px solid #38444D;
            background-color: #192734;
            color: #ffffff;
            border-radius: 5px;
        }
        .modal button {
            padding: 10px 15px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin-right: 10px;
        }
        .modal button#save-settings {
            background-color: #1DA1F2;
            color: white;
            font-weight: bold;
        }
        .modal button#save-settings:hover {
            background-color: #1A91DA;
        }
        .modal button#clear-history {
            background-color: #E0245E;
            color: white;
            font-weight: bold;

        }
        .modal button#clear-history:hover {
            background-color: #C01E4E;

        }
        .modal button#test-webhook {
            background-color: #17BF63;
            color: white;
            font-weight: bold;

        }
        .modal button#test-webhook:hover {
            background-color: #14A358;
        }
        .notification {
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 10px 20px;
            border-radius: 5px;
            font-size: 14px;
            z-index: 10000;
            transition: opacity 0.5s ease-in-out;
            color: white;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }
        .notification.success { background-color: #1DA1F2; }
        .notification.error { background-color: #E0245E; }
        .notification.info { background-color: #17202A; }
        .switch {
            position: relative;
            display: inline-block;
            width: 60px;
            height: 34px;
        }
        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: .4s;
            border-radius: 34px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 26px;
            width: 26px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: .4s;
            border-radius: 50%;
        }
        input:checked + .slider {
            background-color: #1DA1F2;
        }
        input:checked + .slider:before {
            transform: translateX(26px);
        }
        .settings-row {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 20px;
        }
    `);

    function addSendButtonToTweets() {
        const tweets = document.querySelectorAll('button[data-testid="bookmark"]');
        tweets.forEach(bookmarkButton => {
            const parentDiv = bookmarkButton.parentElement;
            const tweet = parentDiv.closest('article[data-testid="tweet"]');
            if (tweet && !tweet.querySelector('.custom-send-icon')) {
                const sendIcon = document.createElement('div');
                sendIcon.classList.add('custom-send-icon');
                sendIcon.setAttribute('aria-label', 'Send to Discord');
                sendIcon.setAttribute('role', 'button');
                sendIcon.setAttribute('tabindex', '0');
                sendIcon.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 19px; height: 19px; border-radius: 9999px; transition-duration: 0.2s; cursor: pointer;';

                const tweetUrl = extractTweetUrl(tweet);
                const tweetId = tweetUrl.split('/').pop();
                const isSent = sentTweets.has(tweetId);

                sendIcon.innerHTML = isSent ? copiedSVG : defaultSVG;

                sendIcon.addEventListener('click', (event) => {
                    event.stopPropagation();
                    if (tweetUrl) {
                        if (!isSent) {
                            sendToDiscord(tweetId, tweetUrl, sendIcon);
                        } else {
                            showNotification('This tweet has already been sent to Discord', 'info');
                        }
                    }
                });

                const parentDivClone = parentDiv.cloneNode(true);
                parentDivClone.style.cssText = 'display: flex; align-items: center;';
                parentDiv.parentNode.insertBefore(parentDivClone, parentDiv.nextSibling);
                parentDivClone.innerHTML = '';
                parentDivClone.appendChild(sendIcon);

                // Add listener to the like button
                const likeButton = tweet.querySelector('[data-testid="like"]');
                if (likeButton) {
                    likeButton.addEventListener('click', (e) => {
                        if (e.target.closest('[data-testid="like"]')) {
                            if (!isSent && config.autoSendEnabled) {
                                sendToDiscord(tweetId, tweetUrl, sendIcon);
                            }
                        }
                    });
                }
            }
        });
    }

    function extractTweetUrl(tweetElement) {
        const linkElement = tweetElement.querySelector('a[href*="/status/"]');
        if (!linkElement) {
            return;
        }
        let url = linkElement.getAttribute('href').split('?')[0]; // Remove any query parameters
        if (url.includes('/photo/')) {
            url = url.split('/photo/')[0];
        }
        return `${baseUrl}${url}`;
    }

    function sendToDiscord(tweetId, link, buttonElement) {
        if (!config.discordWebhookUrl) {
            showNotification('Discord webhook URL is not set. Please set it in the script settings.', 'error');
            return;
        }

        if (sentTweets.has(tweetId)) {
            showNotification('This tweet has already been sent to Discord', 'info');
            return;
        }

        GM_xmlhttpRequest({
            method: 'POST',
            url: config.discordWebhookUrl,
            headers: {
                'Content-Type': 'application/json'
            },
            data: JSON.stringify({ content: link }),
            onload: function(response) {
                if (response.status === 204) {
                    showNotification('Tweet sent to Discord successfully', 'success');
                    sentTweets.add(tweetId);
                    GM_setValue('sentTweets', JSON.stringify([...sentTweets]));
                    buttonElement.innerHTML = copiedSVG;
                } else {
                    showNotification('Failed to send tweet to Discord. Please check your webhook URL.', 'error');
                    console.error('Failed to send tweet to Discord', response);
                }
            },
            onerror: function(error) {
                showNotification('Error sending tweet to Discord. Please check your internet connection.', 'error');
                console.error('Error sending tweet to Discord', error);
            }
        });
    }

    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.textContent = message;
        notification.classList.add('notification', type);
        notification.onclick = () => notification.remove();

        document.body.appendChild(notification);

        setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                document.body.removeChild(notification);
            }, 500);
        }, 3000);
    }

    function createSettingsModal() {
        const modal = document.createElement('div');
        modal.classList.add('modal');
        modal.innerHTML = `
            <div class="modal-content">
                <span class="close">&times;</span>
                <h2>Twitter to Discord Settings</h2>
                <label for="webhook-url">Discord Webhook URL:</label>
                <input type="text" id="webhook-url" value="${config.discordWebhookUrl}">
                <div class="settings-row">
                    <label for="auto-send">Auto-send on like:</label>
                    <label class="switch">
                        <input type="checkbox" id="auto-send" ${config.autoSendEnabled ? 'checked' : ''}>
                        <span class="slider"></span>
                    </label>
                </div>
                <button id="save-settings">Save Settings</button>
                <button id="clear-history">Clear Sent History</button>
                <button id="test-webhook">Test Webhook</button>
            </div>
        `;

        document.body.appendChild(modal);

        const closeBtn = modal.querySelector('.close');
        const saveBtn = modal.querySelector('#save-settings');
        const clearHistoryBtn = modal.querySelector('#clear-history');
        const testWebhookBtn = modal.querySelector('#test-webhook');

        closeBtn.onclick = () => modal.style.display = 'none';
        saveBtn.onclick = saveSettings;
        clearHistoryBtn.onclick = clearSentTweetsHistory;
        testWebhookBtn.onclick = testWebhook;

        window.onclick = (event) => {
            if (event.target === modal) {
                modal.style.display = 'none';
            }
        };

        return modal;
    }

    function saveSettings() {
        const webhookUrl = document.getElementById('webhook-url').value;
        const autoSend = document.getElementById('auto-send').checked;

        config.discordWebhookUrl = webhookUrl;
        config.autoSendEnabled = autoSend;

        GM_setValue('discordWebhookUrl', webhookUrl);
        GM_setValue('autoSendEnabled', autoSend);

        showNotification('Settings saved successfully', 'success');
        document.querySelector('.modal').style.display = 'none';
    }

    function clearSentTweetsHistory() {
        if (confirm('Are you sure you want to clear the history of sent tweets? This will allow re-sending of previously sent tweets.')) {
            sentTweets.clear();
            GM_setValue('sentTweets', '[]');
            showNotification('Sent tweets history has been cleared.', 'success');
        }
    }

    function testWebhook() {
        const webhookUrl = document.getElementById('webhook-url').value;
        if (!webhookUrl) {
            showNotification('Please enter a webhook URL before testing.', 'error');
            return;
        }

        GM_xmlhttpRequest({
            method: 'POST',
            url: webhookUrl,
            headers: {
                'Content-Type': 'application/json'
            },
            data: JSON.stringify({ content: 'Test message from Twitter to Discord script' }),
            onload: function(response) {
                if (response.status === 204) {
                    showNotification('Webhook test successful!', 'success');
                } else {
                    showNotification('Webhook test failed. Please check your URL.', 'error');
                }
            },
            onerror: function(error) {
                showNotification('Error testing webhook. Please check your internet connection.', 'error');
            }
        });
    }

    const settingsModal = createSettingsModal();

    GM_registerMenuCommand('Twitter to Discord Settings', () => {
        settingsModal.style.display = 'block';
    });

    const observer = new MutationObserver(addSendButtonToTweets);
    observer.observe(document.body, { childList: true, subtree: true });

    // Run the script
    document.addEventListener('DOMContentLoaded', addSendButtonToTweets);
})();