Plugin
ViewRaw
Copy and view the raw content/data of any message, channel or guild
1
import "./style.css";2
3
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";4
import { definePluginSettings } from "@api/Settings";5
import { CodeBlock } from "@components/CodeBlock";6
import ErrorBoundary from "@components/ErrorBoundary";7
import { HeadingSecondary } from "@components/Heading";8
import { Margins } from "@components/margins";9
import { Devs } from "@utils/constants";10
import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";11
import { isTruthy } from "@utils/guards";12
import definePlugin, { IconComponent, OptionType } from "@utils/types";13
import { Message } from "@vencord/discord-types";14
import { ChannelStore, GuildRoleStore, Menu, Modal, openModal, UserProfileStore } from "@webpack/common";15
import { MouseEventHandler } from "react";16
17
18
const CopyRawIcon: IconComponent = ({ height = 20, width = 20, className }) => {19
return (20
<svg21
viewBox="0 0 20 20"22
fill="currentColor"23
aria-hidden="true"24
width={width}25
height={height}26
className={className}27
>28
<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" />29
<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" />30
</svg>31
);32
};33
34
function sortObject<T extends object>(obj: T): T {35
return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;36
}37
38
function cleanMessage(msg: Message) {39
const clone = sortObject(JSON.parse(JSON.stringify(msg)));40
for (const key of [41
"email",42
"phone",43
"mfaEnabled",44
"personalConnectionId"45
]) delete clone.author[key];46
47
// message logger added properties48
const cloneAny = clone as any;49
delete cloneAny.editHistory;50
delete cloneAny.deleted;51
delete cloneAny.firstEditTimestamp;52
cloneAny.attachments?.forEach(a => delete a.deleted);53
54
return clone;55
}56
57
function openViewRawModal(json: string, type: string, msgContent?: string) {58
openModal(props => (59
<ErrorBoundary>60
<Modal61
{...props}62
title={`Raw ${type} Data`}63
size="xl"64
actions={[65
{66
text: `Copy ${type} Data`,67
variant: "secondary",68
onClick: () => copyWithToast(json, `${type} data copied to clipboard!`)69
},70
msgContent && {71
text: "Copy Raw Content",72
variant: "secondary",73
onClick: () => copyWithToast(msgContent, "Content copied to clipboard!")74
}75
].filter(isTruthy)}76
>77
{!!msgContent && (78
<>79
<HeadingSecondary>Message Content</HeadingSecondary>80
<CodeBlock className="vc-viewRaw-codeBlock" content={msgContent} lang="" />81
<HeadingSecondary className={Margins.top16}>Message Data</HeadingSecondary>82
</>83
)}84
<CodeBlock className="vc-viewRaw-codeBlock" content={json} lang="json" />85
</Modal>86
</ErrorBoundary >87
));88
}89
90
function openViewRawModalMessage(msg: Message) {91
msg = cleanMessage(msg);92
const msgJson = JSON.stringify(msg, null, 4);93
94
return openViewRawModal(msgJson, "Message", msg.content);95
}96
97
const settings = definePluginSettings({98
clickMethod: {99
description: "Change the button to view the raw content/data of any message.",100
type: OptionType.SELECT,101
options: [102
{ label: "Left Click to view the raw content.", value: "Left", default: true },103
{ label: "Right click to view the raw content.", value: "Right" }104
]105
},106
messageContextMenu: {107
description: "Show in message context menu",108
type: OptionType.BOOLEAN,109
default: false110
}111
});112
113
function MakeContextCallback(name: "Guild" | "Role" | "User" | "Channel" | "Message" | "Profile", getData?: (props: any) => any): NavContextMenuPatchCallback {114
return (children, props) => {115
const value = getData ? getData(props) : props[name.toLowerCase()];116
if (!value) return;117
if (props.label === getIntlMessage("CHANNEL_ACTIONS_MENU_LABEL")) return; class="ts-cmt">// random shit like notification settings118
const isMessage = name === "Message";119
if (isMessage && !settings.store.messageContextMenu) return;120
121
122
// typescript parser goes crazy if this is inline123
const id = `vc-view-${name.toLowerCase()}-raw`;124
const action = isMessage125
? () => openViewRawModalMessage(value)126
: () => openViewRawModal(JSON.stringify(value, null, 4), name);127
128
const devContainer = findGroupChildrenByChildId(`devmode-copy-id-${value.id}`, children);129
130
(devContainer ?? children).splice(-1, 0,131
<Menu.MenuItem132
id={id}133
label="View Raw"134
action={action}135
icon={CopyRawIcon}136
/>137
);138
};139
}140
141
const devContextCallback: NavContextMenuPatchCallback = (children, { id }: { id: string; }) => {142
const guild = getCurrentGuild();143
if (!guild) return;144
145
const role = GuildRoleStore.getRole(guild.id, id);146
if (!role) return;147
148
children.push(149
<Menu.MenuItem150
id={"vc-view-role-raw"}151
label="View Raw"152
action={() => openViewRawModal(JSON.stringify(role, null, 4), "Role")}153
icon={CopyRawIcon}154
/>155
);156
};157
158
export default definePlugin({159
name: "ViewRaw",160
description: "Copy and view the raw content/data of any message, channel or guild",161
tags: ["Chat", "Developers"],162
authors: [Devs.KingFish, Devs.Ven, Devs.rad, Devs.ImLvna],163
settings,164
165
contextMenus: {166
"guild-context": MakeContextCallback("Guild"),167
"guild-settings-role-context": MakeContextCallback("Role"),168
"channel-context": MakeContextCallback("Channel"),169
"thread-context": MakeContextCallback("Channel"),170
"gdm-context": MakeContextCallback("Channel"),171
"user-context": MakeContextCallback("User"),172
"dev-context": devContextCallback,173
"message": MakeContextCallback("Message"),174
"user-profile-overflow-menu": MakeContextCallback("Profile", props => UserProfileStore.getGuildMemberProfile(props.user?.id, props.guildId) ?? UserProfileStore.getUserProfile(props.user?.id))175
},176
177
messagePopoverButton: {178
icon: CopyRawIcon,179
render(msg) {180
const handleClick = () => {181
if (settings.store.clickMethod === "Right") {182
copyWithToast(msg.content);183
} else {184
openViewRawModalMessage(msg);185
}186
};187
188
const handleContextMenu: MouseEventHandler<HTMLButtonElement> = e => {189
if (settings.store.clickMethod === "Left") {190
e.preventDefault();191
e.stopPropagation();192
copyWithToast(msg.content);193
} else {194
e.preventDefault();195
e.stopPropagation();196
openViewRawModalMessage(msg);197
}198
};199
200
const label = settings.store.clickMethod === "Right"201
? "Copy Raw (Left Click) / View Raw (Right Click)"202
: "View Raw (Left Click) / Copy Raw (Right Click)";203
204
return {205
label,206
icon: CopyRawIcon,207
message: msg,208
channel: ChannelStore.getChannel(msg.channel_id),209
onClick: handleClick,210
onContextMenu: handleContextMenu211
};212
}213
}214
});215