// ==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();
}
})();