discord_msg_webhook.user.js
· 23 KiB · JavaScript
Orginalformat
// ==UserScript==
// @name Discord Message to Webhook
// @description Combined tool for viewing JSON data of Discord messages and bulk deletion
// @version 1.0.1
// @author Original scripts by various authors, edited by jirachi
// @match https://*.discord.com/app
// @match https://*.discord.com/channels/*
// @match https://*.discord.com/login
// @license none
// @grant none
// ==/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 = `<svg width="18px" height="18px" viewBox="-10 -5 1034 1034" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M482 226h-1l-10 2q-33 4 -64.5 18.5t-55.5 38.5q-41 37 -57 91q-9 30 -8 63t12 63q17 45 52 78l13 12l-83 135q-26 -1 -45 7q-30 13 -45 40q-7 15 -9 31t2 32q8 30 33 48q15 10 33 14.5t36 2t34.5 -12.5t27.5 -25q12 -17 14.5 -39t-5.5 -41q-1 -5 -7 -14l-3 -6l118 -192 q6 -9 8 -14l-10 -3q-9 -2 -13 -4q-23 -10 -41.5 -27.5t-28.5 -39.5q-17 -36 -9 -75q4 -23 17 -43t31 -34q37 -27 82 -27q27 -1 52.5 9.5t44.5 30.5q17 16 26.5 38.5t10.5 45.5q0 17 -6 42l70 19l8 1q14 -43 7 -86q-4 -33 -19.5 -63.5t-39.5 -53.5q-42 -42 -103 -56 q-6 -2 -18 -4l-14 -2h-37zM500 350q-17 0 -34 7t-30.5 20.5t-19.5 31.5q-8 20 -4 44q3 18 14 34t28 25q24 15 56 13q3 4 5 8l112 191q3 6 6 9q27 -26 58.5 -35.5t65 -3.5t58.5 26q32 25 43.5 61.5t0.5 73.5q-8 28 -28.5 50t-48.5 33q-31 13 -66.5 8.5t-63.5 -24.5 q-4 -3 -13 -10l-5 -6q-4 3 -11 10l-47 46q23 23 52 38.5t61 21.5l22 4h39l28 -5q64 -13 110 -60q22 -22 36.5 -50.5t19.5 -59.5q5 -36 -2 -71.5t-25 -64.5t-44 -51t-57 -35q-34 -14 -70.5 -16t-71.5 7l-17 5l-81 -137q13 -19 16 -37q5 -32 -13 -60q-16 -25 -44 -35 q-17 -6 -35 -6zM218 614q-58 13 -100 53q-47 44 -61 105l-4 24v37l2 11q2 13 4 20q7 31 24.5 59t42.5 49q50 41 115 49q38 4 76 -4.5t70 -28.5q53 -34 78 -91q7 -17 14 -45q6 -1 18 0l125 2q14 0 20 1q11 20 25 31t31.5 16t35.5 4q28 -3 50 -20q27 -21 32 -54 q2 -17 -1.5 -33t-13.5 -30q-16 -22 -41 -32q-17 -7 -35.5 -6.5t-35.5 7.5q-28 12 -43 37l-3 6q-14 0 -42 -1l-113 -1q-15 -1 -43 -1l-50 -1l3 17q8 43 -13 81q-14 27 -40 45t-57 22q-35 6 -70 -7.5t-57 -42.5q-28 -35 -27 -79q1 -37 23 -69q13 -19 32 -32t41 -19l9 -3z"/>
</svg>`;
// --- 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); }
};
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
if (embed.color !== undefined) {
if (typeof embed.color === 'string') {
try {
webhookEmbed.color = parseInt(embed.color.replace('#', ''), 16);
} catch (e) {
console.error("Failed to parse 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 => {
const webhookField = {};
if (field.name) webhookField.name = field.name;
if (field.value) webhookField.value = field.value;
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 = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z" fill="currentColor"/></svg>';
copyButton.innerHTML += '<span style="margin-left: 4px;">Copy</span>';
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 `<span class="${cls}">${match}</span>`;
}
);
// Also highlight brackets and punctuation (but not colons as they're already handled)
highlighted = highlighted.replace(/[{}\[\],]/g, function(match) {
return `<span class="json-punctuation">${match}</span>`;
});
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();
}
})();
1 | // ==UserScript== |
2 | // @name Discord Message to Webhook |
3 | // @description Combined tool for viewing JSON data of Discord messages and bulk deletion |
4 | // @version 1.0.1 |
5 | // @author Original scripts by various authors, edited by jirachi |
6 | // @match https://*.discord.com/app |
7 | // @match https://*.discord.com/channels/* |
8 | // @match https://*.discord.com/login |
9 | // @license none |
10 | // @grant none |
11 | // ==/UserScript== |
12 | |
13 | (function () { |
14 | 'use strict'; |
15 | |
16 | // --- Configuration --- |
17 | let logFn = null; |
18 | let observerThrottle = null; |
19 | |
20 | // --- CSS --- |
21 | const themeCss = ` |
22 | /* JSON Viewer Box */ |
23 | #discord-message-json-display { |
24 | position: fixed; |
25 | top: 50px; |
26 | right: 50px; |
27 | width: 450px; |
28 | height: 300px; |
29 | background-color: var(--background-secondary); |
30 | border: 1px solid var(--background-tertiary); |
31 | border-radius: 5px; |
32 | z-index: 1000; |
33 | overflow: hidden; |
34 | display: flex; |
35 | flex-direction: column; |
36 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; |
37 | font-size: 12px; |
38 | color: var(--text-normal); |
39 | box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.2); |
40 | resize: both; |
41 | overflow: auto; |
42 | min-width: 250px; |
43 | min-height: 150px; |
44 | } |
45 | #discord-message-json-display .header { |
46 | padding: 10px; |
47 | background-color: var(--background-tertiary); |
48 | cursor: grab; |
49 | font-weight: bold; |
50 | border-bottom: 1px solid var(--background-tertiary); |
51 | display: flex; |
52 | justify-content: space-between; |
53 | align-items: center; |
54 | user-select: none; |
55 | } |
56 | #discord-message-json-content { |
57 | flex-grow: 1; |
58 | padding: 10px; |
59 | margin: 0; |
60 | overflow: auto; |
61 | white-space: pre-wrap; |
62 | word-wrap: break-word; |
63 | background-color: var(--background-primary); |
64 | } |
65 | #discord-message-json-display::-webkit-resizer { |
66 | background-color: var(--brand-experiment); |
67 | } |
68 | .format-toggle-container { |
69 | display: flex; |
70 | align-items: center; |
71 | font-weight: normal; |
72 | font-size: 10px; |
73 | margin-right: 10px; |
74 | cursor: pointer; |
75 | } |
76 | .format-toggle-container input { |
77 | margin-right: 5px; |
78 | cursor: pointer; |
79 | } |
80 | |
81 | /* Custom JSON syntax highlighting */ |
82 | .json-string { color: #a8ff60; } |
83 | .json-number { color: #d8a0df; } |
84 | .json-boolean { color: #b285fe; } |
85 | .json-null { color: #70a0d8; } |
86 | .json-key { color: #f08d49; } |
87 | .json-punctuation { color: #7a82da; } |
88 | `; |
89 | |
90 | // --- HTML Elements --- |
91 | // SVG Data for the JSON icon |
92 | const jsonSvgIcon = `<svg width="18px" height="18px" viewBox="-10 -5 1034 1034" xmlns="http://www.w3.org/2000/svg"> |
93 | <path fill="currentColor" d="M482 226h-1l-10 2q-33 4 -64.5 18.5t-55.5 38.5q-41 37 -57 91q-9 30 -8 63t12 63q17 45 52 78l13 12l-83 135q-26 -1 -45 7q-30 13 -45 40q-7 15 -9 31t2 32q8 30 33 48q15 10 33 14.5t36 2t34.5 -12.5t27.5 -25q12 -17 14.5 -39t-5.5 -41q-1 -5 -7 -14l-3 -6l118 -192 q6 -9 8 -14l-10 -3q-9 -2 -13 -4q-23 -10 -41.5 -27.5t-28.5 -39.5q-17 -36 -9 -75q4 -23 17 -43t31 -34q37 -27 82 -27q27 -1 52.5 9.5t44.5 30.5q17 16 26.5 38.5t10.5 45.5q0 17 -6 42l70 19l8 1q14 -43 7 -86q-4 -33 -19.5 -63.5t-39.5 -53.5q-42 -42 -103 -56 q-6 -2 -18 -4l-14 -2h-37zM500 350q-17 0 -34 7t-30.5 20.5t-19.5 31.5q-8 20 -4 44q3 18 14 34t28 25q24 15 56 13q3 4 5 8l112 191q3 6 6 9q27 -26 58.5 -35.5t65 -3.5t58.5 26q32 25 43.5 61.5t0.5 73.5q-8 28 -28.5 50t-48.5 33q-31 13 -66.5 8.5t-63.5 -24.5 q-4 -3 -13 -10l-5 -6q-4 3 -11 10l-47 46q23 23 52 38.5t61 21.5l22 4h39l28 -5q64 -13 110 -60q22 -22 36.5 -50.5t19.5 -59.5q5 -36 -2 -71.5t-25 -64.5t-44 -51t-57 -35q-34 -14 -70.5 -16t-71.5 7l-17 5l-81 -137q13 -19 16 -37q5 -32 -13 -60q-16 -25 -44 -35 q-17 -6 -35 -6zM218 614q-58 13 -100 53q-47 44 -61 105l-4 24v37l2 11q2 13 4 20q7 31 24.5 59t42.5 49q50 41 115 49q38 4 76 -4.5t70 -28.5q53 -34 78 -91q7 -17 14 -45q6 -1 18 0l125 2q14 0 20 1q11 20 25 31t31.5 16t35.5 4q28 -3 50 -20q27 -21 32 -54 q2 -17 -1.5 -33t-13.5 -30q-16 -22 -41 -32q-17 -7 -35.5 -6.5t-35.5 7.5q-28 12 -43 37l-3 6q-14 0 -42 -1l-113 -1q-15 -1 -43 -1l-50 -1l3 17q8 43 -13 81q-14 27 -40 45t-57 22q-35 6 -70 -7.5t-57 -42.5q-28 -35 -27 -79q1 -37 23 -69q13 -19 32 -32t41 -19l9 -3z"/> |
94 | </svg>`; |
95 | |
96 | // --- JSON Viewer Variables --- |
97 | let jsonDisplayBox = null; |
98 | let isDragging = false; |
99 | let dragOffsetX, dragOffsetY; |
100 | let isScriptActive = true; |
101 | let currentMessage = null; |
102 | let isWebhookFormat = false; |
103 | |
104 | // --- Utility Functions --- |
105 | const $ = s => document.querySelector(s); |
106 | |
107 | const log = { |
108 | debug() { return logFn ? logFn('debug', arguments) : console.debug.apply(console, arguments); }, |
109 | info() { return logFn ? logFn('info', arguments) : console.info.apply(console, arguments); }, |
110 | verb() { return logFn ? logFn('verb', arguments) : console.log.apply(console, arguments); }, |
111 | warn() { return logFn ? logFn('warn', arguments) : console.warn.apply(console, arguments); }, |
112 | error() { return logFn ? logFn('error', arguments) : console.error.apply(console, arguments); }, |
113 | success() { return logFn ? logFn('success', arguments) : console.info.apply(console, arguments); } |
114 | }; |
115 | |
116 | const setLogFn = (fn) => logFn = fn; |
117 | |
118 | function createElm(html) { |
119 | const temp = document.createElement('div'); |
120 | temp.innerHTML = html; |
121 | return temp.removeChild(temp.firstElementChild); |
122 | } |
123 | |
124 | function insertCss(css) { |
125 | const style = document.createElement('style'); |
126 | style.innerHTML = css; |
127 | document.head.appendChild(style); |
128 | return style; |
129 | } |
130 | |
131 | // --- React Instance Finders --- |
132 | function findReactInstance(element) { |
133 | for (const key in element) { |
134 | if (key.startsWith('__reactFiber$') || key.startsWith('__reactProps$')) { |
135 | return element[key]; |
136 | } |
137 | } |
138 | return null; |
139 | } |
140 | |
141 | function getMessageFromReactInstance(reactInstance) { |
142 | if (!reactInstance) return null; |
143 | |
144 | let current = reactInstance; |
145 | while (current) { |
146 | if (current.memoizedProps && current.memoizedProps.message) { |
147 | return current.memoizedProps.message; |
148 | } |
149 | current = current.return; |
150 | } |
151 | return null; |
152 | } |
153 | |
154 | // --- JSON Conversion Function --- |
155 | function convertMessageToWebhookJson(message) { |
156 | if (!message) return null; |
157 | |
158 | const webhookJson = {}; |
159 | |
160 | // Basic message properties |
161 | if (message.content) { |
162 | webhookJson.content = message.content; |
163 | } |
164 | // console.log(message); |
165 | // Username and Avatar |
166 | if (message.author) { |
167 | webhookJson.username = message.author.username; |
168 | if (message.author.avatar) { |
169 | const avatarHash = message.author.avatar; |
170 | const userId = message.author.id; |
171 | const isGif = avatarHash.startsWith('a_'); |
172 | webhookJson.avatar_url = `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${isGif ? 'gif' : 'png'}`; |
173 | } else { |
174 | // Default avatar if none is set |
175 | const discriminator = message.author.discriminator; |
176 | let defaultAvatarId; |
177 | if (discriminator === '0') { |
178 | defaultAvatarId = (BigInt(message.author.id) >> 22n) % 6n; |
179 | webhookJson.avatar_url = `https://cdn.discordapp.com/assets/${defaultAvatarId}.png`; |
180 | } else { |
181 | defaultAvatarId = parseInt(discriminator) % 5; |
182 | webhookJson.avatar_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarId}.png`; |
183 | } |
184 | } |
185 | } |
186 | |
187 | // Embeds |
188 | if (message.embeds) { |
189 | |
190 | webhookJson.embeds = message.embeds.map(embed => { |
191 | const webhookEmbed = {}; |
192 | // console.log(embed); |
193 | if (embed.rawTitle) webhookEmbed.title = embed.rawTitle; |
194 | if (embed.title) webhookEmbed.title = embed.title; |
195 | if (embed.rawDescription) webhookEmbed.description = embed.rawDescription; |
196 | if (embed.description) webhookEmbed.description = embed.description; |
197 | if (embed.url) webhookEmbed.url = embed.url; |
198 | |
199 | // Color |
200 | if (embed.color !== undefined) { |
201 | if (typeof embed.color === 'string') { |
202 | try { |
203 | webhookEmbed.color = parseInt(embed.color.replace('#', ''), 16); |
204 | } catch (e) { |
205 | console.error("Failed to parse embed color string:", embed.color, e); |
206 | } |
207 | } else if (typeof embed.color === 'number') { |
208 | webhookEmbed.color = embed.color; |
209 | } |
210 | } |
211 | |
212 | if (embed.timestamp) webhookEmbed.timestamp = embed.timestamp; |
213 | |
214 | if (embed.footer) { |
215 | const webhookFooter = {}; |
216 | if (embed.footer.text) webhookFooter.text = embed.footer.text; |
217 | if (embed.footer.icon_url) webhookFooter.icon_url = embed.footer.icon_url; |
218 | if (Object.keys(webhookFooter).length > 0) webhookEmbed.footer = webhookFooter; |
219 | } |
220 | |
221 | if (embed.image) { |
222 | const webhookImage = {}; |
223 | if (embed.image.url) webhookImage.url = embed.image.url; |
224 | if (Object.keys(webhookImage).length > 0) webhookEmbed.image = webhookImage; |
225 | } |
226 | |
227 | if (embed.thumbnail) { |
228 | const webhookThumbnail = {}; |
229 | if (embed.thumbnail.url) webhookThumbnail.url = embed.thumbnail.url; |
230 | if (Object.keys(webhookThumbnail).length > 0) webhookEmbed.thumbnail = webhookThumbnail; |
231 | } |
232 | |
233 | if (embed.author) { |
234 | const webhookAuthor = {}; |
235 | if (embed.author.name) webhookAuthor.name = embed.author.name; |
236 | if (embed.author.url) webhookAuthor.url = embed.author.url; |
237 | if (embed.author.icon_url) webhookAuthor.icon_url = embed.author.icon_url; |
238 | if (Object.keys(webhookAuthor).length > 0) webhookEmbed.author = webhookAuthor; |
239 | } |
240 | |
241 | if (embed.fields && embed.fields.length > 0) { |
242 | webhookEmbed.fields = embed.fields.map(field => { |
243 | const webhookField = {}; |
244 | if (field.name) webhookField.name = field.name; |
245 | if (field.value) webhookField.value = field.value; |
246 | if (field.inline !== undefined) webhookField.inline = field.inline; |
247 | return webhookField; |
248 | }); |
249 | } |
250 | |
251 | return webhookEmbed; |
252 | }).filter(embed => Object.keys(embed).length > 0); |
253 | } |
254 | |
255 | return webhookJson; |
256 | } |
257 | |
258 | // --- JSON Viewer Functions --- |
259 | function createJsonDisplayBox() { |
260 | jsonDisplayBox = document.createElement('div'); |
261 | jsonDisplayBox.id = 'discord-message-json-display'; |
262 | jsonDisplayBox.style.display = 'none'; // Initially hidden |
263 | |
264 | const header = document.createElement('div'); |
265 | header.className = 'header'; |
266 | // Create a text node as the first child |
267 | header.appendChild(document.createTextNode('Discord Message JSON Data')); |
268 | |
269 | const controls = document.createElement('div'); |
270 | controls.style.cssText = 'display: flex; align-items: center;'; |
271 | |
272 | // Webhook Toggle |
273 | const webhookToggleLabel = document.createElement('label'); |
274 | webhookToggleLabel.style.cssText = 'font-weight: normal; font-size: 10px; margin-right: 10px; display: flex; align-items: center; cursor: pointer;'; |
275 | webhookToggleLabel.textContent = 'Webhook Format'; |
276 | |
277 | const webhookToggle = document.createElement('input'); |
278 | webhookToggle.type = 'checkbox'; |
279 | webhookToggle.style.cssText = 'margin-right: 5px; cursor: pointer;'; |
280 | webhookToggle.addEventListener('change', function() { |
281 | isWebhookFormat = this.checked; |
282 | if (currentMessage) { |
283 | // Only update content, not the entire structure |
284 | const jsonToDisplay = isWebhookFormat ? convertMessageToWebhookJson(currentMessage) : currentMessage; |
285 | const jsonString = jsonToDisplay ? JSON.stringify(jsonToDisplay, null, 2) : 'Could not retrieve message data.'; |
286 | const contentElement = document.getElementById('discord-message-json-content'); |
287 | contentElement.textContent = jsonString; |
288 | |
289 | // Apply our own highlighting |
290 | highlightJsonContent(); |
291 | } |
292 | // Update header text but preserve the DOM structure |
293 | const headerText = document.querySelector('#discord-message-json-display .header'); |
294 | const headerTitle = headerText.childNodes[0]; |
295 | headerTitle.textContent = isWebhookFormat ? 'Discord Webhook JSON Data' : 'Discord Message JSON Data'; |
296 | }); |
297 | |
298 | webhookToggleLabel.prepend(webhookToggle); |
299 | controls.appendChild(webhookToggleLabel); |
300 | |
301 | // Copy Button |
302 | const copyButton = document.createElement('div'); |
303 | copyButton.style.cssText = 'font-weight: normal; font-size: 10px; margin-right: 10px; cursor: pointer; display: flex; align-items: center;'; |
304 | copyButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 1H4C2.9 1 2 1.9 2 3V17H4V3H16V1ZM19 5H8C6.9 5 6 5.9 6 7V21C6 22.1 6.9 23 8 23H19C20.1 23 21 22.1 21 21V7C21 5.9 20.1 5 19 5ZM19 21H8V7H19V21Z" fill="currentColor"/></svg>'; |
305 | copyButton.innerHTML += '<span style="margin-left: 4px;">Copy</span>'; |
306 | |
307 | copyButton.onclick = () => { |
308 | const jsonContent = document.getElementById('discord-message-json-content'); |
309 | |
310 | // Get the plain text content, not the highlighted HTML |
311 | let textToCopy = ''; |
312 | if (currentMessage) { |
313 | const jsonToDisplay = isWebhookFormat ? convertMessageToWebhookJson(currentMessage) : currentMessage; |
314 | textToCopy = JSON.stringify(jsonToDisplay, null, 2); |
315 | } else { |
316 | textToCopy = jsonContent.textContent; |
317 | } |
318 | |
319 | // Copy to clipboard |
320 | navigator.clipboard.writeText(textToCopy) |
321 | .then(() => { |
322 | // Visual feedback |
323 | const originalText = copyButton.querySelector('span').textContent; |
324 | copyButton.querySelector('span').textContent = 'Copied!'; |
325 | setTimeout(() => { |
326 | copyButton.querySelector('span').textContent = originalText; |
327 | }, 1000); |
328 | }) |
329 | .catch(err => { |
330 | console.error('Failed to copy JSON: ', err); |
331 | }); |
332 | }; |
333 | |
334 | controls.appendChild(copyButton); |
335 | |
336 | const closeButton = document.createElement('span'); |
337 | closeButton.style.cssText = 'margin-left: 10px; cursor: pointer; font-size: 16px; color: var(--interactive-normal);'; |
338 | closeButton.textContent = '✕'; |
339 | closeButton.onclick = () => { |
340 | jsonDisplayBox.style.display = 'none'; |
341 | }; |
342 | |
343 | controls.appendChild(closeButton); |
344 | header.appendChild(controls); |
345 | jsonDisplayBox.appendChild(header); |
346 | |
347 | const content = document.createElement('pre'); |
348 | content.id = 'discord-message-json-content'; |
349 | jsonDisplayBox.appendChild(content); |
350 | |
351 | document.body.appendChild(jsonDisplayBox); |
352 | |
353 | // Make the box draggable |
354 | header.addEventListener('mousedown', (e) => { |
355 | if (e.button !== 0) return; |
356 | isDragging = true; |
357 | dragOffsetX = e.clientX - jsonDisplayBox.getBoundingClientRect().left; |
358 | dragOffsetY = e.clientY - jsonDisplayBox.getBoundingClientRect().top; |
359 | jsonDisplayBox.style.cursor = 'grabbing'; |
360 | document.body.style.userSelect = 'none'; |
361 | }); |
362 | |
363 | document.addEventListener('mousemove', (e) => { |
364 | if (!isDragging) return; |
365 | jsonDisplayBox.style.left = (e.clientX - dragOffsetX) + 'px'; |
366 | jsonDisplayBox.style.top = (e.clientY - dragOffsetY) + 'px'; |
367 | }); |
368 | |
369 | document.addEventListener('mouseup', () => { |
370 | isDragging = false; |
371 | if (jsonDisplayBox) jsonDisplayBox.style.cursor = ''; |
372 | document.body.style.userSelect = ''; |
373 | }); |
374 | } |
375 | |
376 | function highlightJsonContent() { |
377 | const codeElement = document.getElementById('discord-message-json-content'); |
378 | if (!codeElement) return; |
379 | |
380 | // Get the text content |
381 | const text = codeElement.textContent; |
382 | |
383 | // JSON syntax highlighting with colons preserved |
384 | let highlighted = text.replace( |
385 | /"(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, |
386 | function (match) { |
387 | let cls = 'json-number'; |
388 | if (/^"/.test(match)) { |
389 | if (/:$/.test(match)) { |
390 | cls = 'json-key'; |
391 | // Don't remove the colon |
392 | } else { |
393 | cls = 'json-string'; |
394 | } |
395 | } else if (/true|false/.test(match)) { |
396 | cls = 'json-boolean'; |
397 | } else if (/null/.test(match)) { |
398 | cls = 'json-null'; |
399 | } |
400 | |
401 | return `<span class="${cls}">${match}</span>`; |
402 | } |
403 | ); |
404 | |
405 | // Also highlight brackets and punctuation (but not colons as they're already handled) |
406 | highlighted = highlighted.replace(/[{}\[\],]/g, function(match) { |
407 | return `<span class="json-punctuation">${match}</span>`; |
408 | }); |
409 | |
410 | codeElement.innerHTML = highlighted; |
411 | } |
412 | |
413 | function displayMessageJson(message) { |
414 | if (!jsonDisplayBox) { |
415 | createJsonDisplayBox(); |
416 | } |
417 | |
418 | currentMessage = message; |
419 | |
420 | let jsonToDisplay = null; |
421 | if (currentMessage) { |
422 | if (isWebhookFormat) { |
423 | jsonToDisplay = convertMessageToWebhookJson(currentMessage); |
424 | } else { |
425 | jsonToDisplay = currentMessage; |
426 | } |
427 | } |
428 | |
429 | const jsonString = jsonToDisplay ? JSON.stringify(jsonToDisplay, null, 2) : 'Could not retrieve message data.'; |
430 | const contentElement = document.getElementById('discord-message-json-content'); |
431 | contentElement.textContent = jsonString; |
432 | |
433 | // Apply our custom syntax highlighting |
434 | highlightJsonContent(); |
435 | |
436 | jsonDisplayBox.style.display = 'flex'; |
437 | } |
438 | |
439 | // Handle message clicks for JSON viewing |
440 | function handleMessageClick(event) { |
441 | if (!isScriptActive) return; |
442 | |
443 | const target = event.target; |
444 | const messageElement = target.closest('[id^="message-"]'); |
445 | |
446 | if (messageElement) { |
447 | // Skip if clicking interactive elements |
448 | const interactiveElements = target.closest('a, button, .markup-2BOw-j'); |
449 | if (interactiveElements && !interactiveElements.classList.contains('embedWrapper-lXp9gn')) { |
450 | return; |
451 | } |
452 | |
453 | const reactInstance = findReactInstance(messageElement); |
454 | if (reactInstance) { |
455 | const message = getMessageFromReactInstance(reactInstance); |
456 | if (message) { |
457 | displayMessageJson(message); |
458 | return; |
459 | } |
460 | } |
461 | |
462 | // log.warn("Could not find React instance for message element."); |
463 | if (jsonDisplayBox && document.getElementById('discord-message-json-content')) { |
464 | document.getElementById('discord-message-json-content').textContent = 'Could not retrieve message data.'; |
465 | currentMessage = null; |
466 | } |
467 | } |
468 | } |
469 | |
470 | // --- Button Creation and Mounting --- |
471 | let jsonViewerBtn = null; |
472 | |
473 | function createJsonViewerButton() { |
474 | // Create button container |
475 | jsonViewerBtn = document.createElement('div'); |
476 | jsonViewerBtn.id = 'json-viewer-btn'; |
477 | jsonViewerBtn.setAttribute('role', 'button'); |
478 | jsonViewerBtn.setAttribute('aria-label', 'View Message JSON'); |
479 | jsonViewerBtn.setAttribute('tabindex', '0'); |
480 | jsonViewerBtn.title = 'View Message JSON'; |
481 | jsonViewerBtn.style.cssText = ` |
482 | position: relative; |
483 | width: auto; |
484 | height: 24px; |
485 | margin: 0 8px; |
486 | cursor: pointer; |
487 | color: var(--interactive-normal); |
488 | flex: 0 0 auto; |
489 | display: flex; |
490 | align-items: center; |
491 | justify-content: center; |
492 | `; |
493 | jsonViewerBtn.innerHTML = jsonSvgIcon; |
494 | |
495 | // Add click handler |
496 | jsonViewerBtn.onclick = () => { |
497 | if (jsonDisplayBox) { |
498 | jsonDisplayBox.style.display = jsonDisplayBox.style.display === 'none' ? 'flex' : 'none'; |
499 | } else { |
500 | createJsonDisplayBox(); |
501 | jsonDisplayBox.style.display = 'flex'; |
502 | } |
503 | }; |
504 | |
505 | // Add hover effects |
506 | jsonViewerBtn.onmouseover = () => { |
507 | jsonViewerBtn.style.color = 'var(--interactive-hover)'; |
508 | }; |
509 | |
510 | jsonViewerBtn.onmouseout = () => { |
511 | jsonViewerBtn.style.color = 'var(--interactive-normal)'; |
512 | }; |
513 | |
514 | return jsonViewerBtn; |
515 | } |
516 | |
517 | function mountJsonViewerButton() { |
518 | const toolbar = document.querySelector('#app-mount [class^=toolbar]'); |
519 | if (toolbar) { |
520 | // Check if button already exists |
521 | if (!document.getElementById('json-viewer-btn')) { |
522 | if (!jsonViewerBtn) { |
523 | jsonViewerBtn = createJsonViewerButton(); |
524 | } |
525 | toolbar.appendChild(jsonViewerBtn); |
526 | console.log('Mounted JSON Viewer button'); |
527 | } |
528 | } |
529 | } |
530 | |
531 | function initJsonViewer() { |
532 | // Insert CSS |
533 | insertCss(themeCss); |
534 | |
535 | // Create button |
536 | createJsonViewerButton(); |
537 | |
538 | // Create display box (initially hidden) |
539 | createJsonDisplayBox(); |
540 | |
541 | // Mount button |
542 | mountJsonViewerButton(); |
543 | |
544 | // Add message click listener |
545 | document.addEventListener('click', handleMessageClick, true); |
546 | |
547 | // Setup observer to re-mount button if needed |
548 | const discordElm = document.querySelector('#app-mount'); |
549 | if (discordElm) { |
550 | const observer = new MutationObserver(() => { |
551 | if (observerThrottle) return; |
552 | observerThrottle = setTimeout(() => { |
553 | observerThrottle = null; |
554 | if (!document.body.contains(jsonViewerBtn)) { |
555 | mountJsonViewerButton(); |
556 | } |
557 | }, 3000); |
558 | }); |
559 | |
560 | observer.observe(discordElm, { childList: true, subtree: true }); |
561 | } |
562 | |
563 | console.log('JSON Viewer initialized successfully'); |
564 | } |
565 | |
566 | // Initialize when the page loads |
567 | if (document.readyState === 'loading') { |
568 | window.addEventListener('DOMContentLoaded', initJsonViewer); |
569 | } else { |
570 | initJsonViewer(); |
571 | } |
572 | })(); |
573 |