您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Send tweets to Discord on like and with custom button, using vxtwitter.com links
// ==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">×</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); })();