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 { 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";10
import { sendMessage } from "@utils/discord";11
import { Logger } from "@utils/Logger";12
import { Margins } from "@utils/margins";13
import { isPluginDev, tryOrElse } from "@utils/misc";14
import { relaunch } from "@utils/native";15
import { onlyOnce } from "@utils/onlyOnce";16
import { makeCodeblock } from "@utils/text";17
import definePlugin from "@utils/types";18
import { checkForUpdates, isOutdated, update } from "@utils/updater";19
import { Channel, RenderModalProps } from "@vencord/discord-types";20
import { Button, ChannelStore, ConfirmModal, Forms, GuildMemberStore, openModal, Parser, PermissionsBits, PermissionStore, RelationshipStore, showToast, Text, Toasts, UserStore } from "@webpack/common";21
import { JSX } from "react";22
23
import gitHash from "~git-hash";24
import plugins, { PluginMeta } from "~plugins";25
26
import SettingsPlugin from "./settings";27
28
const CodeBlockRe = /```js\n(.+?)```/s;29
30
const AdditionalAllowedChannelIds = [31
"1024286218801926184", class="ts-cmt">// Vencord > #bot-commands32
];33
34
const TrustedRolesIds = [35
CONTRIB_ROLE_ID, class="ts-cmt">// contributor36
REGULAR_ROLE_ID, class="ts-cmt">// regular37
DONOR_ROLE_ID, class="ts-cmt">// donor38
];39
40
const AsyncFunction = async function () { }.constructor;41
42
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;43
44
const isSupportAllowedChannel = (channel: Channel) => channel.parent_id === SUPPORT_CATEGORY_ID || AdditionalAllowedChannelIds.includes(channel.id);45
46
async function forceUpdate() {47
const outdated = await checkForUpdates();48
if (outdated) {49
await update();50
relaunch();51
}52
53
return outdated;54
}55
56
async 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-error65
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.platform75
};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
96
function 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
115
const checkForUpdatesOnce = onlyOnce(checkForUpdates);116
117
const settings = definePluginSettings({}).withPrivateSettings<{118
dismissedDevBuildWarning?: boolean;119
}>();120
121
function DevBuildConfirmModal(props: RenderModalProps) {122
const s = settings.use(["dismissedDevBuildWarning"]);123
124
return (125
<ConfirmModal126
{...props}127
title="Hold on!"128
confirmText="Understood"129
variant="primary"130
checkboxProps={{131
checked: s.dismissedDevBuildWarning === true,132
onChange: checked => s.dismissedDevBuildWarning = checked133
}}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
149
export 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
<ConfirmModal195
{...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 you039;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
<ConfirmModal224
{...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>, or233
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_DISABLED253
&& (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
<Button262
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
else269
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 Now277
</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
<Button285
key="vc-dbg"286
color={Button.Colors.PRIMARY}287
onClick={async () => sendMessage(props.channel.id, { content: await generateDebugInfoMessage() })}288
>289
Run /vencord-debug290
</Button>,291
<Button292
key="vc-plg-list"293
color={Button.Colors.PRIMARY}294
onClick={async () => sendMessage(props.channel.id, { content: generatePluginList() })}295
>296
Run /vencord-plugins297
</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
<Button307
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 Snippet319
</Button>320
);321
}322
}323
324
return buttons.length325
? <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