// ==UserScript== // @name Discord Message to Webhook // @description Combined tool for viewing JSON data of Discord messages, based off undiscord // @version 1.0.5 // @author Original scripts by various authors, edited by shayne // @match https://*.discord.com/app // @match https://*.discord.com/channels/* // @match https://*.discord.com/login // @license none // @grant none // @downloadURL https://gist.yorgei.dev/yorgei22/1a8dc2789b484f879d271c90885b1026/raw/HEAD/discord_msg_webhook.user.js // ==/UserScript== (function () { 'use strict'; // --- Configuration --- let logFn = null; let observerThrottle = null; // --- CSS --- const themeCss = ` /* JSON Viewer Box */ #discord-message-json-display { position: fixed; top: 50px; right: 50px; width: 450px; height: 300px; background-color: var(--background-secondary); border: 1px solid var(--background-tertiary); border-radius: 5px; z-index: 1000; overflow: hidden; display: flex; flex-direction: column; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-size: 12px; color: var(--text-normal); box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2); resize: both; overflow: auto; min-width: 250px; min-height: 150px; } #discord-message-json-display .header { padding: 10px; background-color: var(--background-tertiary); cursor: grab; font-weight: bold; border-bottom: 1px solid var(--background-tertiary); display: flex; justify-content: space-between; align-items: center; user-select: none; } #discord-message-json-content { flex-grow: 1; padding: 10px; margin: 0; overflow: auto; white-space: pre-wrap; word-wrap: break-word; background-color: var(--background-primary); } #discord-message-json-display::-webkit-resizer { background-color: var(--brand-experiment); } .format-toggle-container { display: flex; align-items: center; font-weight: normal; font-size: 10px; margin-right: 10px; cursor: pointer; } .format-toggle-container input { margin-right: 5px; cursor: pointer; } /* Custom JSON syntax highlighting */ .json-string { color: #a8ff60; } .json-number { color: #d8a0df; } .json-boolean { color: #b285fe; } .json-null { color: #70a0d8; } .json-key { color: #f08d49; } .json-punctuation { color: #7a82da; } `; // --- HTML Elements --- // SVG Data for the JSON icon const jsonSvgIcon = ` `; // --- JSON Viewer Variables --- let jsonDisplayBox = null; let isDragging = false; let dragOffsetX, dragOffsetY; let isScriptActive = true; let currentMessage = null; let isWebhookFormat = false; // --- Utility Functions --- const $ = s => document.querySelector(s); const log = { debug() { return logFn ? logFn('debug', arguments) : console.debug.apply(console, arguments); }, info() { return logFn ? logFn('info', arguments) : console.info.apply(console, arguments); }, verb() { return logFn ? logFn('verb', arguments) : console.log.apply(console, arguments); }, warn() { return logFn ? logFn('warn', arguments) : console.warn.apply(console, arguments); }, error() { return logFn ? logFn('error', arguments) : console.error.apply(console, arguments); }, success() { return logFn ? logFn('success', arguments) : console.info.apply(console, arguments); } }; function hslaToHex(hsla) { // Handle the specific calc variable case by replacing it with 100% const processedHsla = hsla.replace(/calc\(var\(--saturation-factor,\s*1\)\s*\*\s*100%\)/g, '100%'); // Parse the HSLA values const parts = processedHsla.match(/hsla?\((\d+),\s*(\d+(\.\d+)?%),\s*(\d+(\.\d+)?%),\s*(\d?(\.\d+)?)\)/); if (!parts) { // If parsing fails, return a default color or handle the error console.error("Failed to parse HSLA string:", hsla); return null; // Or return a default hex color like 0 } const h = parseInt(parts[1], 10); const s = parseFloat(parts[2]) / 100; const l = parseFloat(parts[4]) / 100; const a = parseFloat(parts[7] !== undefined ? parts[7] : 1); // Default alpha to 1 if not present // Convert HSL to RGB let r, g, b; if (s === 0) { r = g = b = l; // Achromatic } else { const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h / 360 + 1 / 3); g = hue2rgb(p, q, h / 360); b = hue2rgb(p, q, h / 360 - 1 / 3); } // Convert RGB to Hex const toHex = x => { const hex = Math.round(x * 255).toString(16); return hex.length === 1 ? '0' + hex : hex; }; const hex = `${toHex(r)}${toHex(g)}${toHex(b)}`; // Handle Alpha (optional, as webhook colors are typically RGB hex) // If you need to include alpha in the hex (RRGGBBAA), uncomment the following: /* const toHexAlpha = x => { const hexA = Math.round(x * 255).toString(16); return hexA.length === 1 ? '0' + hexA : hexA; }; return parseInt(hex + toHexAlpha(a), 16); */ return parseInt(hex, 16); } const setLogFn = (fn) => logFn = fn; function createElm(html) { const temp = document.createElement('div'); temp.innerHTML = html; return temp.removeChild(temp.firstElementChild); } function insertCss(css) { const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); return style; } // --- React Instance Finders --- function findReactInstance(element) { for (const key in element) { if (key.startsWith('__reactFiber$') || key.startsWith('__reactProps$')) { return element[key]; } } return null; } function getMessageFromReactInstance(reactInstance) { if (!reactInstance) return null; let current = reactInstance; while (current) { if (current.memoizedProps && current.memoizedProps.message) { return current.memoizedProps.message; } current = current.return; } return null; } // --- JSON Conversion Function --- function convertMessageToWebhookJson(message) { if (!message) return null; const webhookJson = {}; // Basic message properties if (message.content) { webhookJson.content = message.content; } // console.log(message); // Username and Avatar if (message.author) { webhookJson.username = message.author.username; if (message.author.avatar) { const avatarHash = message.author.avatar; const userId = message.author.id; const isGif = avatarHash.startsWith('a_'); webhookJson.avatar_url = `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${isGif ? 'gif' : 'png'}`; } else { // Default avatar if none is set const discriminator = message.author.discriminator; let defaultAvatarId; if (discriminator === '0') { defaultAvatarId = (BigInt(message.author.id) >> 22n) % 6n; webhookJson.avatar_url = `https://cdn.discordapp.com/assets/${defaultAvatarId}.png`; } else { defaultAvatarId = parseInt(discriminator) % 5; webhookJson.avatar_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarId}.png`; } } } // Embeds if (message.embeds) { webhookJson.embeds = message.embeds.map(embed => { const webhookEmbed = {}; // console.log(embed); if (embed.rawTitle) webhookEmbed.title = embed.rawTitle; if (embed.title) webhookEmbed.title = embed.title; if (embed.rawDescription) webhookEmbed.description = embed.rawDescription; if (embed.description) webhookEmbed.description = embed.description; if (embed.url) webhookEmbed.url = embed.url; // Color // Original snippet with the added HSLA handling if (embed.color !== undefined) { if (typeof embed.color === 'string') { try { // Check if the color string is an HSLA value if (embed.color.startsWith('hsl')) { const hexColor = hslaToHex(embed.color); if (hexColor !== null) { webhookEmbed.color = hexColor; } else { // Handle the case where HSLA parsing failed console.error("Could not convert HSLA color to hex:", embed.color); // Optionally set a default color or leave it undefined webhookEmbed.color = 0; // Example: set to black } } else { // Assume it's a hex string if not HSLA webhookEmbed.color = parseInt(embed.color.replace('#', ''), 16); } } catch (e) { console.error("Failed to process embed color string:", embed.color, e); } } else if (typeof embed.color === 'number') { webhookEmbed.color = embed.color; } } if (embed.timestamp) webhookEmbed.timestamp = embed.timestamp; if (embed.footer) { const webhookFooter = {}; if (embed.footer.text) webhookFooter.text = embed.footer.text; if (embed.footer.icon_url) webhookFooter.icon_url = embed.footer.icon_url; if (Object.keys(webhookFooter).length > 0) webhookEmbed.footer = webhookFooter; } if (embed.image) { const webhookImage = {}; if (embed.image.url) webhookImage.url = embed.image.url; if (Object.keys(webhookImage).length > 0) webhookEmbed.image = webhookImage; } if (embed.thumbnail) { const webhookThumbnail = {}; if (embed.thumbnail.url) webhookThumbnail.url = embed.thumbnail.url; if (Object.keys(webhookThumbnail).length > 0) webhookEmbed.thumbnail = webhookThumbnail; } if (embed.author) { const webhookAuthor = {}; if (embed.author.name) webhookAuthor.name = embed.author.name; if (embed.author.url) webhookAuthor.url = embed.author.url; if (embed.author.icon_url) webhookAuthor.icon_url = embed.author.icon_url; if (Object.keys(webhookAuthor).length > 0) webhookEmbed.author = webhookAuthor; } if (embed.fields && embed.fields.length > 0) { webhookEmbed.fields = embed.fields.map(field => { console.log(field); const webhookField = {}; if (field.rawName) webhookField.name = field.rawName; if (field.rawValue) webhookField.value = field.rawValue; if (field.inline !== undefined) webhookField.inline = field.inline; return webhookField; }); } return webhookEmbed; }).filter(embed => Object.keys(embed).length > 0); } return webhookJson; } // --- JSON Viewer Functions --- function createJsonDisplayBox() { jsonDisplayBox = document.createElement('div'); jsonDisplayBox.id = 'discord-message-json-display'; jsonDisplayBox.style.display = 'none'; // Initially hidden const header = document.createElement('div'); header.className = 'header'; // Create a text node as the first child header.appendChild(document.createTextNode('Discord Message JSON Data')); const controls = document.createElement('div'); controls.style.cssText = 'display: flex; align-items: center;'; // Webhook Toggle const webhookToggleLabel = document.createElement('label'); webhookToggleLabel.style.cssText = 'font-weight: normal; font-size: 10px; margin-right: 10px; display: flex; align-items: center; cursor: pointer;'; webhookToggleLabel.textContent = 'Webhook Format'; const webhookToggle = document.createElement('input'); webhookToggle.type = 'checkbox'; webhookToggle.style.cssText = 'margin-right: 5px; cursor: pointer;'; webhookToggle.addEventListener('change', function() { isWebhookFormat = this.checked; if (currentMessage) { // Only update content, not the entire structure const jsonToDisplay = isWebhookFormat ? convertMessageToWebhookJson(currentMessage) : currentMessage; const jsonString = jsonToDisplay ? JSON.stringify(jsonToDisplay, null, 2) : 'Could not retrieve message data.'; const contentElement = document.getElementById('discord-message-json-content'); contentElement.textContent = jsonString; // Apply our own highlighting highlightJsonContent(); } // Update header text but preserve the DOM structure const headerText = document.querySelector('#discord-message-json-display .header'); const headerTitle = headerText.childNodes[0]; headerTitle.textContent = isWebhookFormat ? 'Discord Webhook JSON Data' : 'Discord Message JSON Data'; }); webhookToggleLabel.prepend(webhookToggle); controls.appendChild(webhookToggleLabel); // Copy Button const copyButton = document.createElement('div'); copyButton.style.cssText = 'font-weight: normal; font-size: 10px; margin-right: 10px; cursor: pointer; display: flex; align-items: center;'; copyButton.innerHTML = ''; copyButton.innerHTML += 'Copy'; copyButton.onclick = () => { const jsonContent = document.getElementById('discord-message-json-content'); // Get the plain text content, not the highlighted HTML let textToCopy = ''; if (currentMessage) { const jsonToDisplay = isWebhookFormat ? convertMessageToWebhookJson(currentMessage) : currentMessage; textToCopy = JSON.stringify(jsonToDisplay, null, 2); } else { textToCopy = jsonContent.textContent; } // Copy to clipboard navigator.clipboard.writeText(textToCopy) .then(() => { // Visual feedback const originalText = copyButton.querySelector('span').textContent; copyButton.querySelector('span').textContent = 'Copied!'; setTimeout(() => { copyButton.querySelector('span').textContent = originalText; }, 1000); }) .catch(err => { console.error('Failed to copy JSON: ', err); }); }; controls.appendChild(copyButton); const closeButton = document.createElement('span'); closeButton.style.cssText = 'margin-left: 10px; cursor: pointer; font-size: 16px; color: var(--interactive-normal);'; closeButton.textContent = '✕'; closeButton.onclick = () => { jsonDisplayBox.style.display = 'none'; }; controls.appendChild(closeButton); header.appendChild(controls); jsonDisplayBox.appendChild(header); const content = document.createElement('pre'); content.id = 'discord-message-json-content'; jsonDisplayBox.appendChild(content); document.body.appendChild(jsonDisplayBox); // Make the box draggable header.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isDragging = true; dragOffsetX = e.clientX - jsonDisplayBox.getBoundingClientRect().left; dragOffsetY = e.clientY - jsonDisplayBox.getBoundingClientRect().top; jsonDisplayBox.style.cursor = 'grabbing'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; jsonDisplayBox.style.left = (e.clientX - dragOffsetX) + 'px'; jsonDisplayBox.style.top = (e.clientY - dragOffsetY) + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; if (jsonDisplayBox) jsonDisplayBox.style.cursor = ''; document.body.style.userSelect = ''; }); } function highlightJsonContent() { const codeElement = document.getElementById('discord-message-json-content'); if (!codeElement) return; // Get the text content const text = codeElement.textContent; // JSON syntax highlighting with colons preserved let highlighted = text.replace( /"(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, function (match) { let cls = 'json-number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; // Don't remove the colon } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } return `${match}`; } ); // Also highlight brackets and punctuation (but not colons as they're already handled) highlighted = highlighted.replace(/[{}\[\],]/g, function(match) { return `${match}`; }); codeElement.innerHTML = highlighted; } function displayMessageJson(message) { if (!jsonDisplayBox) { createJsonDisplayBox(); } currentMessage = message; let jsonToDisplay = null; if (currentMessage) { if (isWebhookFormat) { jsonToDisplay = convertMessageToWebhookJson(currentMessage); } else { jsonToDisplay = currentMessage; } } const jsonString = jsonToDisplay ? JSON.stringify(jsonToDisplay, null, 2) : 'Could not retrieve message data.'; const contentElement = document.getElementById('discord-message-json-content'); contentElement.textContent = jsonString; // Apply our custom syntax highlighting highlightJsonContent(); jsonDisplayBox.style.display = 'flex'; } // Handle message clicks for JSON viewing function handleMessageClick(event) { if (!isScriptActive) return; const target = event.target; const messageElement = target.closest('[id^="message-"]'); if (messageElement) { // Skip if clicking interactive elements const interactiveElements = target.closest('a, button, .markup-2BOw-j'); if (interactiveElements && !interactiveElements.classList.contains('embedWrapper-lXp9gn')) { return; } const reactInstance = findReactInstance(messageElement); if (reactInstance) { const message = getMessageFromReactInstance(reactInstance); if (message) { displayMessageJson(message); return; } } // log.warn("Could not find React instance for message element."); if (jsonDisplayBox && document.getElementById('discord-message-json-content')) { document.getElementById('discord-message-json-content').textContent = 'Could not retrieve message data.'; currentMessage = null; } } } // --- Button Creation and Mounting --- let jsonViewerBtn = null; function createJsonViewerButton() { // Create button container jsonViewerBtn = document.createElement('div'); jsonViewerBtn.id = 'json-viewer-btn'; jsonViewerBtn.setAttribute('role', 'button'); jsonViewerBtn.setAttribute('aria-label', 'View Message JSON'); jsonViewerBtn.setAttribute('tabindex', '0'); jsonViewerBtn.title = 'View Message JSON'; jsonViewerBtn.style.cssText = ` position: relative; width: auto; height: 24px; margin: 0 8px; cursor: pointer; color: var(--interactive-normal); flex: 0 0 auto; display: flex; align-items: center; justify-content: center; `; jsonViewerBtn.innerHTML = jsonSvgIcon; // Add click handler jsonViewerBtn.onclick = () => { if (jsonDisplayBox) { jsonDisplayBox.style.display = jsonDisplayBox.style.display === 'none' ? 'flex' : 'none'; } else { createJsonDisplayBox(); jsonDisplayBox.style.display = 'flex'; } }; // Add hover effects jsonViewerBtn.onmouseover = () => { jsonViewerBtn.style.color = 'var(--interactive-hover)'; }; jsonViewerBtn.onmouseout = () => { jsonViewerBtn.style.color = 'var(--interactive-normal)'; }; return jsonViewerBtn; } function mountJsonViewerButton() { const toolbar = document.querySelector('#app-mount [class^=toolbar]'); if (toolbar) { // Check if button already exists if (!document.getElementById('json-viewer-btn')) { if (!jsonViewerBtn) { jsonViewerBtn = createJsonViewerButton(); } toolbar.appendChild(jsonViewerBtn); console.log('Mounted JSON Viewer button'); } } } function initJsonViewer() { // Insert CSS insertCss(themeCss); // Create button createJsonViewerButton(); // Create display box (initially hidden) createJsonDisplayBox(); // Mount button mountJsonViewerButton(); // Add message click listener document.addEventListener('click', handleMessageClick, true); // Setup observer to re-mount button if needed const discordElm = document.querySelector('#app-mount'); if (discordElm) { const observer = new MutationObserver(() => { if (observerThrottle) return; observerThrottle = setTimeout(() => { observerThrottle = null; if (!document.body.contains(jsonViewerBtn)) { mountJsonViewerButton(); } }, 3000); }); observer.observe(discordElm, { childList: true, subtree: true }); } console.log('JSON Viewer initialized successfully'); } // Initialize when the page loads if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', initJsonViewer); } else { initJsonViewer(); } })();