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