Plugin

ViewRaw

Copy and view the raw content/data of any message, channel or guild

Chat Developers
index.tsx
Download

Source

src/plugins/viewRaw/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./style.css";
8
9import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
10import { definePluginSettings } from "@api/Settings";
11import { CodeBlock } from "@components/CodeBlock";
12import ErrorBoundary from "@components/ErrorBoundary";
13import { HeadingSecondary } from "@components/Heading";
14import { Margins } from "@components/margins";
15import { Devs } from "@utils/constants";
16import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
17import { isTruthy } from "@utils/guards";
18import definePlugin, { IconComponent, OptionType } from "@utils/types";
19import { Message } from "@vencord/discord-types";
20import { ChannelStore, GuildRoleStore, Menu, Modal, openModal, UserProfileStore } from "@webpack/common";
21import { MouseEventHandler } from "react";
22
23
24const CopyRawIcon: IconComponent = ({ height = 20, width = 20, className }) => {
25 return (
26 <svg
27 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
40function sortObject<T extends object>(obj: T): T {
41 return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;
42}
43
44function 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 properties
54 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
63function openViewRawModal(json: string, type: string, msgContent?: string) {
64 openModal(props => (
65 <ErrorBoundary>
66 <Modal
67 {...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
96function 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
103const 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: false
116 }
117});
118
119function 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 settings
124 const isMessage = name === "Message";
125 if (isMessage && !settings.store.messageContextMenu) return;
126
127
128 // typescript parser goes crazy if this is inline
129 const id = `vc-view-${name.toLowerCase()}-raw`;
130 const action = isMessage
131 ? () => 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.MenuItem
138 id={id}
139 label="View Raw"
140 action={action}
141 icon={CopyRawIcon}
142 />
143 );
144 };
145}
146
147const 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.MenuItem
156 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
164export 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: handleContextMenu
217 };
218 }
219 }
220});
221