Plugin
SupportHelper
Helps us provide support to you
1
import { isPluginEnabled } from "@api/PluginManager";2
import { definePluginSettings } from "@api/Settings";3
import { getUserSettingLazy } from "@api/UserSettings";4
import { Card } from "@components/Card";5
import ErrorBoundary from "@components/ErrorBoundary";6
import { Flex } from "@components/Flex";7
import { Link } from "@components/Link";8
import { openSettingsTabModal, UpdaterTab } from "@components/settings";9
import { BRAND_DISCORD, BRAND_NAME, BRAND_RELEASES, BRAND_WEBSITE } from "@utils/branding";10
import { 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";11
import { sendMessage } from "@utils/discord";12
import { Logger } from "@utils/Logger";13
import { Margins } from "@utils/margins";14
import { isPluginDev, tryOrElse } from "@utils/misc";15
import { relaunch } from "@utils/native";16
import { onlyOnce } from "@utils/onlyOnce";17
import { makeCodeblock } from "@utils/text";18
import definePlugin from "@utils/types";19
import { checkForUpdates, isOutdated, update } from "@utils/updater";20
import { Channel, RenderModalProps } from "@vencord/discord-types";21
import { Button, ChannelStore, ConfirmModal, Forms, GuildMemberStore, openModal, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";22
import { JSX } from "react";23
24
import gitHash from "~git-hash";25
import plugins, { PluginMeta } from "~plugins";26
27
import SettingsPlugin from "./settings";28
29
const CodeBlockRe = /```js\n(.+?)```/s;30
31
const AdditionalAllowedChannelIds = [32
"1024286218801926184", class="ts-cmt">// Vencord > #bot-commands33
];34
35
const TrustedRolesIds = [36
CONTRIB_ROLE_ID, class="ts-cmt">// contributor37
REGULAR_ROLE_ID, class="ts-cmt">// regular38
DONOR_ROLE_ID, class="ts-cmt">// donor39
];40
41
const AsyncFunction = async function () { }.constructor;42
43
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;44
45
const isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);46
47
async function forceUpdate() {48
const outdated = await checkForUpdates();49
if (outdated) {50
await update();51
relaunch();52
}53
54
return outdated;55
}56
57
async 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-error66
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.platform76
};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
97
function 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
116
const checkForUpdatesOnce = onlyOnce(checkForUpdates);117
118
const settings = definePluginSettings({}).withPrivateSettings<{119
dismissedDevBuildWarning?: boolean;120
}>();121
122
function DevBuildConfirmModal(props: RenderModalProps) {123
const s = settings.use(["dismissedDevBuildWarning"]);124
125
return (126
<ConfirmModal127
{...props}128
title="Hold on!"129
confirmText="Understood"130
variant="primary"131
checkboxProps={{132
checked: s.dismissedDevBuildWarning === true,133
onChange: checked => s.dismissedDevBuildWarning = checked134
}}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
150
export 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
<ConfirmModal196
{...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 you039;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
<ConfirmModal225
{...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>, or234
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_DISABLED254
&& (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
<Button263
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
else270
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 Now278
</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
<Button286
key="vc-dbg"287
color={Button.Colors.PRIMARY}288
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}289
>290
Run /fivecord-debug291
</Button>,292
<Button293
key="vc-plg-list"294
color={Button.Colors.PRIMARY}295
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}296
>297
Run /fivecord-plugins298
</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
<Button308
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 Snippet320
</Button>321
);322
}323
}324
325
return buttons.length326
? <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