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
1import "./style.css";
2
3import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
4import { definePluginSettings } from "@api/Settings";
5import { CodeBlock } from "@components/CodeBlock";
6import ErrorBoundary from "@components/ErrorBoundary";
7import { HeadingSecondary } from "@components/Heading";
8import { Margins } from "@components/margins";
9import { Devs } from "@utils/constants";
10import { copyWithToast, getCurrentGuild, getIntlMessage } from "@utils/discord";
11import { isTruthy } from "@utils/guards";
12import definePlugin, { IconComponent, OptionType } from "@utils/types";
13import { Message } from "@vencord/discord-types";
14import { ChannelStore, GuildRoleStore, Menu, Modal, openModal, UserProfileStore } from "@webpack/common";
15import { MouseEventHandler } from "react";
16
17
18const CopyRawIcon: IconComponent = ({ height = 20, width = 20, className }) => {
19 return (
20 <svg
21 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
34function sortObject<T extends object>(obj: T): T {
35 return Object.fromEntries(Object.entries(obj).sort(([k1], [k2]) => k1.localeCompare(k2))) as T;
36}
37
38function 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 properties
48 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
57function openViewRawModal(json: string, type: string, msgContent?: string) {
58 openModal(props => (
59 <ErrorBoundary>
60 <Modal
61 {...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
90function 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
97const 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: false
110 }
111});
112
113function 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 settings
118 const isMessage = name === "Message";
119 if (isMessage && !settings.store.messageContextMenu) return;
120
121
122 // typescript parser goes crazy if this is inline
123 const id = `vc-view-${name.toLowerCase()}-raw`;
124 const action = isMessage
125 ? () => 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.MenuItem
132 id={id}
133 label="View Raw"
134 action={action}
135 icon={CopyRawIcon}
136 />
137 );
138 };
139}
140
141const 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.MenuItem
150 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
158export 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: handleContextMenu
211 };
212 }
213 }
214});
215