Plugin

SupportHelper

Helps us provide support to you

index.tsx
Download

Source

src/plugins/_core/supportHelper/index.tsx
1import { isPluginEnabled } from "@api/PluginManager";
2import { definePluginSettings } from "@api/Settings";
3import { getUserSettingLazy } from "@api/UserSettings";
4import { Card } from "@components/Card";
5import ErrorBoundary from "@components/ErrorBoundary";
6import { Flex } from "@components/Flex";
7import { Link } from "@components/Link";
8import { openSettingsTabModal, UpdaterTab } from "@components/settings";
9import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, KNOWN_ISSUES_CHANNEL_ID, REGULAR_ROLE_ID, SUPPORT_CATEGORY_ID, SUPPORT_CHANNEL_ID, VENBOT_USER_ID, VENCORD_GUILD_ID } from "@utils/constants";
10import { sendMessage } from "@utils/discord";
11import { Logger } from "@utils/Logger";
12import { Margins } from "@utils/margins";
13import { isPluginDev, tryOrElse } from "@utils/misc";
14import { relaunch } from "@utils/native";
15import { onlyOnce } from "@utils/onlyOnce";
16import { makeCodeblock } from "@utils/text";
17import definePlugin from "@utils/types";
18import { checkForUpdates, isOutdated, update } from "@utils/updater";
19import { Channel, RenderModalProps } from "@vencord/discord-types";
20import { Button, ChannelStore, ConfirmModal, Forms, GuildMemberStore, openModal, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";
21import { JSX } from "react";
22
23import gitHash from "~git-hash";
24import plugins, { PluginMeta } from "~plugins";
25
26import SettingsPlugin from "./settings";
27
28const CodeBlockRe = /```js\n(.+?)```/s;
29
30const AdditionalAllowedChannelIds = [
31 "1024286218801926184", class="ts-cmt">// Vencord > #bot-commands
32];
33
34const TrustedRolesIds = [
35 CONTRIB_ROLE_ID, class="ts-cmt">// contributor
36 REGULAR_ROLE_ID, class="ts-cmt">// regular
37 DONOR_ROLE_ID, class="ts-cmt">// donor
38];
39
40const AsyncFunction = async function () { }.constructor;
41
42const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
43
44const isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);
45
46async function forceUpdate() {
47 const outdated = await checkForUpdates();
48 if (outdated) {
49 await update();
50 relaunch();
51 }
52
53 return outdated;
54}
55
56async function generateDebugInfoMessage() {
57 const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
58
59 const client = (() => {
60 if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`;
61 if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`;
62 if ("legcord" in window) return `Legcord v${window.legcord.version}`;
63
64 // @ts-expect-error
65 const name = typeof unsafeWindow !== "undefined" ? "UserScript" : "Web";
66 return `${name} (${navigator.userAgent})`;
67 })();
68
69 const info = {
70 Vencord:
71 `v${VERSION} • [${gitHash}](<https:class="ts-cmt">//github.com/Vendicated/Vencord/commit/${gitHash}>)` +
72 `${SettingsPlugin.additionalInfo} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`,
73 Client: `${RELEASE_CHANNEL} ~ ${client}`,
74 Platform: navigator.platform
75 };
76
77 if (IS_DISCORD_DESKTOP) {
78 info["Last Crash Reason"] = (await tryOrElse(() => DiscordNative.processUtils.getLastCrash(), undefined))?.rendererCrashReason ?? "N/A";
79 }
80
81 const commonIssues = {
82 "Activity Sharing disabled": tryOrElse(() => !ShowCurrentGame.getSetting(), false),
83 "Vencord DevBuild": !IS_STANDALONE,
84 "Has UserPlugins": Object.values(PluginMeta).some(m => m.userPlugin),
85 "More than two weeks out of date": BUILD_TIMESTAMP < Date.now() - 12096e5,
86 };
87
88 let content = `>>> ${Object.entries(info).map(([k, v]) => `**${k}**: ${v}`).join("\n")}`;
89 content += "\n" + Object.entries(commonIssues)
90 .filter(([, v]) => v).map(([k]) => `⚠️ ${k}`)
91 .join("\n");
92
93 return content.trim();
94}
95
96function generatePluginList() {
97 const isApiPlugin = (plugin: string) => plugin.endsWith("API") || plugins[plugin].required;
98
99 const enabledPlugins = Object.keys(plugins)
100 .filter(p => isPluginEnabled(p) && !isApiPlugin(p));
101
102 const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin);
103 const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin);
104
105
106 let content = `**Enabled Plugins (${enabledStockPlugins.length}):**\n${makeCodeblock(enabledStockPlugins.join(", "))}`;
107
108 if (enabledUserPlugins.length) {
109 content += `**Enabled UserPlugins (${enabledUserPlugins.length}):**\n${makeCodeblock(enabledUserPlugins.join(", "))}`;
110 }
111
112 return content;
113}
114
115const checkForUpdatesOnce = onlyOnce(checkForUpdates);
116
117const settings = definePluginSettings({}).withPrivateSettings<{
118 dismissedDevBuildWarning?: boolean;
119}>();
120
121function DevBuildConfirmModal(props: RenderModalProps) {
122 const s = settings.use(["dismissedDevBuildWarning"]);
123
124 return (
125 <ConfirmModal
126 {...props}
127 title="Hold on!"
128 confirmText="Understood"
129 variant="primary"
130 checkboxProps={{
131 checked: s.dismissedDevBuildWarning === true,
132 onChange: checked => s.dismissedDevBuildWarning = checked
133 }}
134 >
135 <div>
136 <Forms.FormText>You are using a custom build of Vencord, which we do not provide support for!</Forms.FormText>
137
138 <Forms.FormText className={Margins.top8}>
139 We only provide support for <Link href="https:class="ts-cmt">//vencord.dev/download">official builds</Link>.
140 Either <Link href="https:class="ts-cmt">//vencord.dev/download">switch to an official build</Link> or figure your issue out yourself.
141 </Forms.FormText>
142
143 <Text variant="text-md/bold" className={Margins.top8}>You will be banned from receiving support if you ignore this rule.</Text>
144 </div>
145 </ConfirmModal>
146 );
147}
148
149export default definePlugin({
150 name: "SupportHelper",
151 required: true,
152 description: "Helps us provide support to you",
153 authors: [Devs.Ven],
154 dependencies: ["UserSettingsAPI"],
155
156 settings,
157
158 patches: [{
159 find: "#{intl::BEGINNING_DM}",
160 replacement: {
161 match: /#{intl::BEGINNING_DM},{.+?}\),(?=.{0,300}(\i)\.isMultiUserDM)/,
162 replace: "$& $self.renderContributorDmWarningCard({ channel: $1 }),"
163 }
164 }],
165
166 commands: [
167 {
168 name: "vencord-debug",
169 description: "Send Vencord debug info",
170 predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
171 execute: async () => ({ content: await generateDebugInfoMessage() })
172 },
173 {
174 name: "vencord-plugins",
175 description: "Send Vencord plugin list",
176 predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || isSupportAllowedChannel(ctx.channel),
177 execute: () => ({ content: generatePluginList() })
178 }
179 ],
180
181 flux: {
182 async CHANNEL_SELECT({ channelId }) {
183 const isSupportChannel = channelId === SUPPORT_CHANNEL_ID || ChannelStore.getChannel(channelId)?.parent_id === SUPPORT_CATEGORY_ID;
184 if (!isSupportChannel) return;
185
186 const selfId = UserStore.getCurrentUser()?.id;
187 if (!selfId || isPluginDev(selfId)) return;
188
189 if (!IS_UPDATER_DISABLED) {
190 await checkForUpdatesOnce().catch(() => { });
191
192 if (isOutdated) {
193 openModal(props => (
194 <ConfirmModal
195 {...props}
196 variant="primary"
197 title="Hold on!"
198 confirmText="Update & Restart Now"
199 cancelText="View Updates"
200 onConfirm={forceUpdate}
201 onCancel={() => openSettingsTabModal(UpdaterTab!)}
202 >
203 <div>
204 <Forms.FormText>You are using an outdated version of Vencord! Chances are, your issue is already fixed.</Forms.FormText>
205 <Forms.FormText className={Margins.top8}>
206 Please first update before asking for support!
207 </Forms.FormText>
208 <Forms.FormText className={Margins.top8}>
209 If you know what you&#039;re doing or cannot update, you can dismiss this prompt.
210 </Forms.FormText>
211 </div>
212 </ConfirmModal>
213 ));
214 return;
215 }
216 }
217
218 const roles = GuildMemberStore.getSelfMember(VENCORD_GUILD_ID)?.roles;
219 if (!roles || TrustedRolesIds.some(id => roles.includes(id))) return;
220
221 if (!IS_WEB && IS_UPDATER_DISABLED) {
222 openModal(props => (
223 <ConfirmModal
224 {...props}
225 title="Hold on!"
226 confirmText="OK"
227 variant="primary"
228 >
229 <div>
230 <Forms.FormText>You are using an externally updated Vencord version, which we do not provide support for!</Forms.FormText>
231 <Forms.FormText className={Margins.top8}>
232 Please either switch to an <Link href="https:class="ts-cmt">//vencord.dev/download">officially supported version of Vencord</Link>, or
233 contact your package maintainer for support instead.
234 </Forms.FormText>
235 </div>
236 </ConfirmModal>
237 ));
238 return;
239 }
240
241 if (!IS_STANDALONE && !settings.store.dismissedDevBuildWarning) {
242 openModal(props => <DevBuildConfirmModal {...props} />);
243 return;
244 }
245 }
246 },
247
248 renderMessageAccessory(props) {
249 const buttons = [] as JSX.Element[];
250
251 const shouldAddUpdateButton =
252 !IS_UPDATER_DISABLED
253 && (
254 (props.channel.id === KNOWN_ISSUES_CHANNEL_ID) ||
255 (props.channel.parent_id === SUPPORT_CATEGORY_ID && props.message.author.id === VENBOT_USER_ID)
256 )
257 && props.message.content?.toLowerCase().includes("update");
258
259 if (shouldAddUpdateButton) {
260 buttons.push(
261 <Button
262 key="vc-update"
263 color={Button.Colors.GREEN}
264 onClick={async () => {
265 try {
266 if (await forceUpdate())
267 showToast("Success! Restarting...", Toasts.Type.SUCCESS);
268 else
269 showToast("Already up to date!", Toasts.Type.MESSAGE);
270 } catch (e) {
271 new Logger(this.name).error("Error while updating:", e);
272 showToast("Failed to update :(", Toasts.Type.FAILURE);
273 }
274 }}
275 >
276 Update Now
277 </Button>
278 );
279 }
280
281 if (props.channel.parent_id === SUPPORT_CATEGORY_ID && PermissionStore.can(PermissionsBits.SEND_MESSAGES, props.channel)) {
282 if (props.message.content.includes("/vencord-debug") || props.message.content.includes("/vencord-plugins")) {
283 buttons.push(
284 <Button
285 key="vc-dbg"
286 color={Button.Colors.PRIMARY}
287 onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}
288 >
289 Run /vencord-debug
290 </Button>,
291 <Button
292 key="vc-plg-list"
293 color={Button.Colors.PRIMARY}
294 onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}
295 >
296 Run /vencord-plugins
297 </Button>
298 );
299 }
300 }
301
302 if (props.channel.parent_id === KNOWN_ISSUES_CHANNEL_ID || (props.channel.parent_id === SUPPORT_CATEGORY_ID && props.message.author.id === VENBOT_USER_ID)) {
303 const match = CodeBlockRe.exec(props.message.content || props.message.embeds[0]?.rawDescription || "");
304 if (match) {
305 buttons.push(
306 <Button
307 key="vc-run-snippet"
308 onClick={async () => {
309 try {
310 await AsyncFunction(match[1])();
311 showToast("Success!", Toasts.Type.SUCCESS);
312 } catch (e) {
313 new Logger(this.name).error("Error while running snippet:", e);
314 showToast("Failed to run snippet :(", Toasts.Type.FAILURE);
315 }
316 }}
317 >
318 Run Snippet
319 </Button>
320 );
321 }
322 }
323
324 return buttons.length
325 ? <Flex>{buttons}</Flex>
326 : null;
327 },
328
329 renderContributorDmWarningCard: ErrorBoundary.wrap(({ channel }) => {
330 const userId = channel.getRecipientId();
331 if (!isPluginDev(userId)) return null;
332 if (RelationshipStore.isFriend(userId) || isPluginDev(UserStore.getCurrentUser()?.id)) return null;
333
334 return (
335 <Card variant="warning" className={Margins.top8} defaultPadding>
336 Please do not private message Vencord plugin developers for support!
337 <br />
338 Instead, use the Vencord support channel: {Parser.parse("https:class="ts-cmt">//discord.com/channels/1015060230222131221/1026515880080842772")}
339 {!ChannelStore.getChannel(SUPPORT_CHANNEL_ID) && " (Click the link to join)"}
340 </Card>
341 );
342 }, { noop: true }),
343});
344