Twitter Like and Send to Discord

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

  1. // ==UserScript==
  2. // @name Twitter Like and Send to Discord
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Send tweets to Discord on like and with custom button, using vxtwitter.com links
  6. // @match https://twitter.com/*
  7. // @match https://x.com/*
  8. // @author dr.bobo0
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_addStyle
  14. // @license MIT
  15. // @connect discord.com
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. let config = {
  22. discordWebhookUrl: GM_getValue('discordWebhookUrl', ''),
  23. autoSendEnabled: GM_getValue('autoSendEnabled', true)
  24. };
  25. let sentTweets = new Set(JSON.parse(GM_getValue('sentTweets', '[]')));
  26.  
  27. const baseUrl = 'https://vxtwitter.com';
  28. 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>';
  29. 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>';
  30.  
  31. GM_addStyle(`
  32. .modal {
  33. display: none;
  34. position: fixed;
  35. z-index: 10000;
  36. left: 0;
  37. top: 0;
  38. width: 100%;
  39. height: 100%;
  40. background-color: rgba(0,0,0,0.4);
  41. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  42. }
  43. .modal-content {
  44. background-color: #15202B;
  45. color: #ffffff;
  46. margin: 15% auto;
  47. padding: 20px;
  48. border: 1px solid #38444D;
  49. border-radius: 15px;
  50. width: 80%;
  51. max-width: 500px;
  52. }
  53. .close {
  54. color: #8899A6;
  55. float: right;
  56. font-size: 28px;
  57. font-weight: bold;
  58. cursor: pointer;
  59. }
  60. .close:hover,
  61. .close:focus {
  62. color: #1DA1F2;
  63. text-decoration: none;
  64. cursor: pointer;
  65. }
  66. .modal h2 {
  67. color: #ffffff;
  68. margin-bottom: 20px;
  69. }
  70. .modal label {
  71. display: block;
  72. margin-bottom: 10px;
  73. color: #8899A6;
  74. }
  75. .modal input[type="text"] {
  76. width: 100%;
  77. padding: 8px;
  78. margin-bottom: 20px;
  79. border: 1px solid #38444D;
  80. background-color: #192734;
  81. color: #ffffff;
  82. border-radius: 5px;
  83. }
  84. .modal button {
  85. padding: 10px 15px;
  86. border: none;
  87. border-radius: 5px;
  88. cursor: pointer;
  89. margin-right: 10px;
  90. }
  91. .modal button#save-settings {
  92. background-color: #1DA1F2;
  93. color: white;
  94. font-weight: bold;
  95. }
  96. .modal button#save-settings:hover {
  97. background-color: #1A91DA;
  98. }
  99. .modal button#clear-history {
  100. background-color: #E0245E;
  101. color: white;
  102. font-weight: bold;
  103.  
  104. }
  105. .modal button#clear-history:hover {
  106. background-color: #C01E4E;
  107.  
  108. }
  109. .modal button#test-webhook {
  110. background-color: #17BF63;
  111. color: white;
  112. font-weight: bold;
  113.  
  114. }
  115. .modal button#test-webhook:hover {
  116. background-color: #14A358;
  117. }
  118. .notification {
  119. position: fixed;
  120. bottom: 20px;
  121. right: 20px;
  122. padding: 10px 20px;
  123. border-radius: 5px;
  124. font-size: 14px;
  125. z-index: 10000;
  126. transition: opacity 0.5s ease-in-out;
  127. color: white;
  128. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  129. }
  130. .notification.success { background-color: #1DA1F2; }
  131. .notification.error { background-color: #E0245E; }
  132. .notification.info { background-color: #17202A; }
  133. .switch {
  134. position: relative;
  135. display: inline-block;
  136. width: 60px;
  137. height: 34px;
  138. }
  139. .switch input {
  140. opacity: 0;
  141. width: 0;
  142. height: 0;
  143. }
  144. .slider {
  145. position: absolute;
  146. cursor: pointer;
  147. top: 0;
  148. left: 0;
  149. right: 0;
  150. bottom: 0;
  151. background-color: #ccc;
  152. transition: .4s;
  153. border-radius: 34px;
  154. }
  155. .slider:before {
  156. position: absolute;
  157. content: "";
  158. height: 26px;
  159. width: 26px;
  160. left: 4px;
  161. bottom: 4px;
  162. background-color: white;
  163. transition: .4s;
  164. border-radius: 50%;
  165. }
  166. input:checked + .slider {
  167. background-color: #1DA1F2;
  168. }
  169. input:checked + .slider:before {
  170. transform: translateX(26px);
  171. }
  172. .settings-row {
  173. display: flex;
  174. align-items: center;
  175. justify-content: space-between;
  176. margin-bottom: 20px;
  177. }
  178. `);
  179.  
  180. function addSendButtonToTweets() {
  181. const tweets = document.querySelectorAll('button[data-testid="bookmark"]');
  182. tweets.forEach(bookmarkButton => {
  183. const parentDiv = bookmarkButton.parentElement;
  184. const tweet = parentDiv.closest('article[data-testid="tweet"]');
  185. if (tweet && !tweet.querySelector('.custom-send-icon')) {
  186. const sendIcon = document.createElement('div');
  187. sendIcon.classList.add('custom-send-icon');
  188. sendIcon.setAttribute('aria-label', 'Send to Discord');
  189. sendIcon.setAttribute('role', 'button');
  190. sendIcon.setAttribute('tabindex', '0');
  191. sendIcon.style.cssText = 'display: flex; align-items: center; justify-content: center; width: 19px; height: 19px; border-radius: 9999px; transition-duration: 0.2s; cursor: pointer;';
  192.  
  193. const tweetUrl = extractTweetUrl(tweet);
  194. const tweetId = tweetUrl.split('/').pop();
  195. const isSent = sentTweets.has(tweetId);
  196.  
  197. sendIcon.innerHTML = isSent ? copiedSVG : defaultSVG;
  198.  
  199. sendIcon.addEventListener('click', (event) => {
  200. event.stopPropagation();
  201. if (tweetUrl) {
  202. if (!isSent) {
  203. sendToDiscord(tweetId, tweetUrl, sendIcon);
  204. } else {
  205. showNotification('This tweet has already been sent to Discord', 'info');
  206. }
  207. }
  208. });
  209.  
  210. const parentDivClone = parentDiv.cloneNode(true);
  211. parentDivClone.style.cssText = 'display: flex; align-items: center;';
  212. parentDiv.parentNode.insertBefore(parentDivClone, parentDiv.nextSibling);
  213. parentDivClone.innerHTML = '';
  214. parentDivClone.appendChild(sendIcon);
  215.  
  216. // Add listener to the like button
  217. const likeButton = tweet.querySelector('[data-testid="like"]');
  218. if (likeButton) {
  219. likeButton.addEventListener('click', (e) => {
  220. if (e.target.closest('[data-testid="like"]')) {
  221. if (!isSent && config.autoSendEnabled) {
  222. sendToDiscord(tweetId, tweetUrl, sendIcon);
  223. }
  224. }
  225. });
  226. }
  227. }
  228. });
  229. }
  230.  
  231. function extractTweetUrl(tweetElement) {
  232. const linkElement = tweetElement.querySelector('a[href*="/status/"]');
  233. if (!linkElement) {
  234. return;
  235. }
  236. let url = linkElement.getAttribute('href').split('?')[0]; // Remove any query parameters
  237. if (url.includes('/photo/')) {
  238. url = url.split('/photo/')[0];
  239. }
  240. return `${baseUrl}${url}`;
  241. }
  242.  
  243. function sendToDiscord(tweetId, link, buttonElement) {
  244. if (!config.discordWebhookUrl) {
  245. showNotification('Discord webhook URL is not set. Please set it in the script settings.', 'error');
  246. return;
  247. }
  248.  
  249. if (sentTweets.has(tweetId)) {
  250. showNotification('This tweet has already been sent to Discord', 'info');
  251. return;
  252. }
  253.  
  254. GM_xmlhttpRequest({
  255. method: 'POST',
  256. url: config.discordWebhookUrl,
  257. headers: {
  258. 'Content-Type': 'application/json'
  259. },
  260. data: JSON.stringify({ content: link }),
  261. onload: function(response) {
  262. if (response.status === 204) {
  263. showNotification('Tweet sent to Discord successfully', 'success');
  264. sentTweets.add(tweetId);
  265. GM_setValue('sentTweets', JSON.stringify([...sentTweets]));
  266. buttonElement.innerHTML = copiedSVG;
  267. } else {
  268. showNotification('Failed to send tweet to Discord. Please check your webhook URL.', 'error');
  269. console.error('Failed to send tweet to Discord', response);
  270. }
  271. },
  272. onerror: function(error) {
  273. showNotification('Error sending tweet to Discord. Please check your internet connection.', 'error');
  274. console.error('Error sending tweet to Discord', error);
  275. }
  276. });
  277. }
  278.  
  279. function showNotification(message, type = 'info') {
  280. const notification = document.createElement('div');
  281. notification.textContent = message;
  282. notification.classList.add('notification', type);
  283. notification.onclick = () => notification.remove();
  284.  
  285. document.body.appendChild(notification);
  286.  
  287. setTimeout(() => {
  288. notification.style.opacity = '0';
  289. setTimeout(() => {
  290. document.body.removeChild(notification);
  291. }, 500);
  292. }, 3000);
  293. }
  294.  
  295. function createSettingsModal() {
  296. const modal = document.createElement('div');
  297. modal.classList.add('modal');
  298. modal.innerHTML = `
  299. <div class="modal-content">
  300. <span class="close">&times;</span>
  301. <h2>Twitter to Discord Settings</h2>
  302. <label for="webhook-url">Discord Webhook URL:</label>
  303. <input type="text" id="webhook-url" value="${config.discordWebhookUrl}">
  304. <div class="settings-row">
  305. <label for="auto-send">Auto-send on like:</label>
  306. <label class="switch">
  307. <input type="checkbox" id="auto-send" ${config.autoSendEnabled ? 'checked' : ''}>
  308. <span class="slider"></span>
  309. </label>
  310. </div>
  311. <button id="save-settings">Save Settings</button>
  312. <button id="clear-history">Clear Sent History</button>
  313. <button id="test-webhook">Test Webhook</button>
  314. </div>
  315. `;
  316.  
  317. document.body.appendChild(modal);
  318.  
  319. const closeBtn = modal.querySelector('.close');
  320. const saveBtn = modal.querySelector('#save-settings');
  321. const clearHistoryBtn = modal.querySelector('#clear-history');
  322. const testWebhookBtn = modal.querySelector('#test-webhook');
  323.  
  324. closeBtn.onclick = () => modal.style.display = 'none';
  325. saveBtn.onclick = saveSettings;
  326. clearHistoryBtn.onclick = clearSentTweetsHistory;
  327. testWebhookBtn.onclick = testWebhook;
  328.  
  329. window.onclick = (event) => {
  330. if (event.target === modal) {
  331. modal.style.display = 'none';
  332. }
  333. };
  334.  
  335. return modal;
  336. }
  337.  
  338. function saveSettings() {
  339. const webhookUrl = document.getElementById('webhook-url').value;
  340. const autoSend = document.getElementById('auto-send').checked;
  341.  
  342. config.discordWebhookUrl = webhookUrl;
  343. config.autoSendEnabled = autoSend;
  344.  
  345. GM_setValue('discordWebhookUrl', webhookUrl);
  346. GM_setValue('autoSendEnabled', autoSend);
  347.  
  348. showNotification('Settings saved successfully', 'success');
  349. document.querySelector('.modal').style.display = 'none';
  350. }
  351.  
  352. function clearSentTweetsHistory() {
  353. if (confirm('Are you sure you want to clear the history of sent tweets? This will allow re-sending of previously sent tweets.')) {
  354. sentTweets.clear();
  355. GM_setValue('sentTweets', '[]');
  356. showNotification('Sent tweets history has been cleared.', 'success');
  357. }
  358. }
  359.  
  360. function testWebhook() {
  361. const webhookUrl = document.getElementById('webhook-url').value;
  362. if (!webhookUrl) {
  363. showNotification('Please enter a webhook URL before testing.', 'error');
  364. return;
  365. }
  366.  
  367. GM_xmlhttpRequest({
  368. method: 'POST',
  369. url: webhookUrl,
  370. headers: {
  371. 'Content-Type': 'application/json'
  372. },
  373. data: JSON.stringify({ content: 'Test message from Twitter to Discord script' }),
  374. onload: function(response) {
  375. if (response.status === 204) {
  376. showNotification('Webhook test successful!', 'success');
  377. } else {
  378. showNotification('Webhook test failed. Please check your URL.', 'error');
  379. }
  380. },
  381. onerror: function(error) {
  382. showNotification('Error testing webhook. Please check your internet connection.', 'error');
  383. }
  384. });
  385. }
  386.  
  387. const settingsModal = createSettingsModal();
  388.  
  389. GM_registerMenuCommand('Twitter to Discord Settings', () => {
  390. settingsModal.style.display = 'block';
  391. });
  392.  
  393. const observer = new MutationObserver(addSendButtonToTweets);
  394. observer.observe(document.body, { childList: true, subtree: true });
  395.  
  396. // Run the script
  397. document.addEventListener('DOMContentLoaded', addSendButtonToTweets);
  398. })();