Plugin
ViewRaw
Copy and view the raw content/data of any message, channel or guild
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import "./style.css";8
9
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";10
import { definePluginSettings } from "@api/Settings";11
import { CodeBlock } from "@components/CodeBlock";12
import ErrorBoundary from "@components/ErrorBoundary";13
import { HeadingSecondary } from "@components/Heading";14
import { Margins } from "@components/margins";15
import { Devs } from "@utils/constants";16
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";17
import { isTruthy } from "@utils/guards";18
import definePlugin, { IconComponent, OptionType } from "@utils/types";19
import { Message } from "@vencord/discord-types";20
import { ChannelStore, GuildRoleStore, Menu, Modal, openModal, UserProfileStore } from "@webpack/common";21
import { MouseEventHandler } from "react";22
23
24
const CopyRawIcon: IconComponent = ({ height = 20, width = 20, className }) => {25
return (26
<svg27
viewBox="0 0 20 20"28
fill="currentColor"29
aria-hidden="true"30
width={width}31
height={height}32
className={className}33
>34
<path d="M12.9297 3.25007C12.7343 3.05261 12.4154 3.05226 12.2196 3.24928L11.5746 3.89824C11.3811 4.09297 11.3808 4.40733 11.5739 4.60245L16.5685 9.64824C16.7614 9.84309 16.7614 10.1569 16.5685 10.3517L11.5739 15.3975C11.3808 15.5927 11.3811 15.907 11.5746 16.1017L12.2196 16.7507C12.4154 16.9477 12.7343 16.9474 12.9297 16.7499L19.2604 10.3517C19.4532 10.1568 19.4532 9.84314 19.2604 9.64832L12.9297 3.25007Z" />35
<path d="M8.42616 4.60245C8.6193 4.40733 8.61898 4.09297 8.42545 3.89824L7.78047 3.24928C7.58466 3.05226 7.26578 3.05261 7.07041 3.25007L0.739669 9.64832C0.5469 9.84314 0.546901 10.1568 0.739669 10.3517L7.07041 16.7499C7.26578 16.9474 7.58465 16.9477 7.78047 16.7507L8.42545 16.1017C8.61898 15.907 8.6193 15.5927 8.42616 15.3975L3.43155 10.3517C3.23869 10.1569 3.23869 9.84309 3.43155 9.64824L8.42616 4.60245Z" />36
</svg>37
);38
};39
40
function sortObject<T extends object>(obj: T): T {41
return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;42
}43
44
function cleanMessage(msg: Message) {45
const clone = sortObject(JSON.parse(JSON.stringify(msg)));46
for (const key of [47
"email",48
"phone",49
"mfaEnabled",50
"personalConnectionId"51
]) delete clone.author[key];52
53
// message logger added properties54
const cloneAny = clone as any;55
delete cloneAny.editHistory;56
delete cloneAny.deleted;57
delete cloneAny.firstEditTimestamp;58
cloneAny.attachments?.forEach(a => delete a.deleted);59
60
return clone;61
}62
63
function openViewRawModal(json: string, type: string, msgContent?: string) {64
openModal(props => (65
<ErrorBoundary>66
<Modal67
{...props}68
title={`Raw ${type} Data`}69
size="xl"70
actions={[71
{72
text: `Copy ${type} Data`,73
variant: "secondary",74
onClick: () => copyWithToast(json, `${type} data copied to clipboard!`)75
},76
msgContent && {77
text: "Copy Raw Content",78
variant: "secondary",79
onClick: () => copyWithToast(msgContent, "Content copied to clipboard!")80
}81
].filter(isTruthy)}82
>83
{!!msgContent && (84
<>85
<HeadingSecondary>Message Content</HeadingSecondary>86
<CodeBlock className="vc-viewRaw-codeBlock" content={msgContent} lang="" />87
<HeadingSecondary className={Margins.top16}>Message Data</HeadingSecondary>88
</>89
)}90
<CodeBlock className="vc-viewRaw-codeBlock" content={json} lang="json" />91
</Modal>92
</ErrorBoundary >93
));94
}95
96
function openViewRawModalMessage(msg: Message) {97
msg = cleanMessage(msg);98
const msgJson = JSON.stringify(msg, null, 4);99
100
return openViewRawModal(msgJson, "Message", msg.content);101
}102
103
const settings = definePluginSettings({104
clickMethod: {105
description: "Change the button to view the raw content/data of any message.",106
type: OptionType.SELECT,107
options: [108
{ label: "Left Click to view the raw content.", value: "Left", default: true },109
{ label: "Right click to view the raw content.", value: "Right" }110
]111
},112
messageContextMenu: {113
description: "Show in message context menu",114
type: OptionType.BOOLEAN,115
default: false116
}117
});118
119
function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel" | "Message" | "Profile", getData?: (props: any) => any): NavContextMenuPatchCallback {120
return (children, props) => {121
const value = getData ? getData(props) : props[name.toLowerCase()];122
if (!value) return;123
if (props.label === getIntlMessage("CHANNEL_ACTIONS_MENU_LABEL")) return; class="ts-cmt">// random shit like notification settings124
const isMessage = name === "Message";125
if (isMessage && !settings.store.messageContextMenu) return;126
127
128
// typescript parser goes crazy if this is inline129
const id = `vc-view-${name.toLowerCase()}-raw`;130
const action = isMessage131
? () => openViewRawModalMessage(value)132
: () => openViewRawModal(JSON.stringify(value, null, 4), name);133
134
const devContainer = findGroupChildrenByChildId(`devmode-copy-id-${value.id}`, children);135
136
(devContainer ?? children).splice(-1, 0,137
<Menu.MenuItem138
id={id}139
label="View Raw"140
action={action}141
icon={CopyRawIcon}142
/>143
);144
};145
}146
147
const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {148
const guild = getCurrentGuild();149
if (!guild) return;150
151
const role = GuildRoleStore.getRole(guild.id, id);152
if (!role) return;153
154
children.push(155
<Menu.MenuItem156
id={"vc-view-role-raw"}157
label="View Raw"158
action={() => openViewRawModal(JSON.stringify(role, null, 4), "Role")}159
icon={CopyRawIcon}160
/>161
);162
};163
164
export default definePlugin({165
name: "ViewRaw",166
description: "Copy and view the raw content/data of any message, channel or guild",167
tags: ["Chat", "Developers"],168
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],169
settings,170
171
contextMenus: {172
"guild-context": MakeContextCallback("Guild"),173
"guild-settings-role-context": MakeContextCallback("Role"),174
"channel-context": MakeContextCallback("Channel"),175
"thread-context": MakeContextCallback("Channel"),176
"gdm-context": MakeContextCallback("Channel"),177
"user-context": MakeContextCallback("User"),178
"dev-context": devContextCallback,179
"message": MakeContextCallback("Message"),180
"user-profile-overflow-menu": MakeContextCallback("Profile", props => UserProfileStore.getGuildMemberProfile(props.user?.id, props.guildId) ?? UserProfileStore.getUserProfile(props.user?.id))181
},182
183
messagePopoverButton: {184
icon: CopyRawIcon,185
render(msg) {186
const handleClick = () => {187
if (settings.store.clickMethod === "Right") {188
copyWithToast(msg.content);189
} else {190
openViewRawModalMessage(msg);191
}192
};193
194
const handleContextMenu: MouseEventHandler<HTMLButtonElement> = e => {195
if (settings.store.clickMethod === "Left") {196
e.preventDefault();197
e.stopPropagation();198
copyWithToast(msg.content);199
} else {200
e.preventDefault();201
e.stopPropagation();202
openViewRawModalMessage(msg);203
}204
};205
206
const label = settings.store.clickMethod === "Right"207
? "Copy Raw (Left Click) / View Raw (Right Click)"208
: "View Raw (Left Click) / Copy Raw (Right Click)";209
210
return {211
label,212
icon: CopyRawIcon,213
message: msg,214
channel: ChannelStore.getChannel(msg.channel_id),215
onClick: handleClick,216
onContextMenu: handleContextMenu217
};218
}219
}220
});221