Plugin
CustomRPC
Add a fully customisable Rich Presence (Game status) to your Discord profile
1
/*2
* FiveCord โ a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { definePluginSettings } from "@api/Settings";8
import { getUserSettingLazy } from "@api/UserSettings";9
import { Divider } from "@components/Divider";10
import { ErrorCard } from "@components/ErrorCard";11
import { Flex } from "@components/Flex";12
import { Link } from "@components/Link";13
import { Devs } from "@utils/constants";14
import { isTruthy } from "@utils/guards";15
import { Margins } from "@utils/margins";16
import { classes } from "@utils/misc";17
import { useAwaiter } from "@utils/react";18
import definePlugin, { OptionType } from "@utils/types";19
import { Activity } from "@vencord/discord-types";20
import { ActivityType } from "@vencord/discord-types/enums";21
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";22
import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";23
24
import { RPCSettings } from "./RpcSettings";25
26
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");27
const ActivityView = findComponentByCodeLazy(".party?(0", "USER_PROFILE_ACTIVITY");28
29
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;30
31
async function getApplicationAsset(key: string): Promise<string> {32
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];33
}34
35
export const enum TimestampMode {36
NONE,37
NOW,38
TIME,39
CUSTOM,40
}41
42
export const settings = definePluginSettings({43
config: {44
type: OptionType.COMPONENT,45
component: RPCSettings46
},47
}).withPrivateSettings<{48
appID?: string;49
appName?: string;50
details?: string;51
detailsURL?: string;52
state?: string;53
stateURL?: string;54
type?: ActivityType;55
streamLink?: string;56
timestampMode?: TimestampMode;57
startTime?: number;58
endTime?: number;59
imageBig?: string;60
imageBigURL?: string;61
imageBigTooltip?: string;62
imageSmall?: string;63
imageSmallURL?: string;64
imageSmallTooltip?: string;65
buttonOneText?: string;66
buttonOneURL?: string;67
buttonTwoText?: string;68
buttonTwoURL?: string;69
partySize?: number;70
partyMaxSize?: number;71
}>();72
73
async function createActivity(): Promise<Activity | undefined> {74
const {75
appID,76
appName,77
details,78
detailsURL,79
state,80
stateURL,81
type,82
streamLink,83
startTime,84
endTime,85
imageBig,86
imageBigURL,87
imageBigTooltip,88
imageSmall,89
imageSmallURL,90
imageSmallTooltip,91
buttonOneText,92
buttonOneURL,93
buttonTwoText,94
buttonTwoURL,95
partyMaxSize,96
partySize,97
timestampMode98
} = settings.store;99
100
if (!appName) return;101
102
const activity: Activity = {103
application_id: appID || "0",104
name: appName,105
state,106
details,107
type: type ?? ActivityType.PLAYING,108
flags: 1 << 0,109
};110
111
if (type === ActivityType.STREAMING) activity.url = streamLink;112
113
switch (timestampMode) {114
case TimestampMode.NOW:115
activity.timestamps = {116
start: Date.now()117
};118
break;119
case TimestampMode.TIME:120
activity.timestamps = {121
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000122
};123
break;124
case TimestampMode.CUSTOM:125
if (startTime || endTime) {126
activity.timestamps = {};127
if (startTime) activity.timestamps.start = startTime;128
if (endTime) activity.timestamps.end = endTime;129
}130
break;131
case TimestampMode.NONE:132
default:133
break;134
}135
136
if (detailsURL) {137
activity.details_url = detailsURL;138
}139
140
if (stateURL) {141
activity.state_url = stateURL;142
}143
144
if (buttonOneText) {145
activity.buttons = [146
buttonOneText,147
buttonTwoText148
].filter(isTruthy);149
150
activity.metadata = {151
button_urls: [152
buttonOneURL,153
buttonTwoURL154
].filter(isTruthy)155
};156
}157
158
if (imageBig) {159
activity.assets = {160
large_image: await getApplicationAsset(imageBig),161
large_text: imageBigTooltip || undefined,162
large_url: imageBigURL || undefined163
};164
}165
166
if (imageSmall) {167
activity.assets = {168
...activity.assets,169
small_image: await getApplicationAsset(imageSmall),170
small_text: imageSmallTooltip || undefined,171
small_url: imageSmallURL || undefined172
};173
}174
175
if (partyMaxSize && partySize) {176
activity.party = {177
size: [partySize, partyMaxSize]178
};179
}180
181
for (const k in activity) {182
if (k === "type") continue;183
const v = activity[k];184
if (!v || v.length === 0)185
delete activity[k];186
}187
188
return activity;189
}190
191
export async function setRpc(disable?: boolean) {192
const activity: Activity | undefined = await createActivity();193
194
FluxDispatcher.dispatch({195
type: "LOCAL_ACTIVITY_UPDATE",196
activity: !disable ? activity : null,197
socketId: "CustomRPC",198
});199
}200
201
export default definePlugin({202
name: "CustomRPC",203
description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",204
tags: ["Activity", "Customisation"],205
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],206
dependencies: ["UserSettingsAPI"],207
// This plugin's patch is not important for functionality, so don't require a restart208
requiresRestart: false,209
settings,210
211
start: setRpc,212
stop: () => setRpc(true),213
214
// Discord hides buttons on your own Rich Presence for some reason. This patch disables that behaviour215
patches: [216
{217
find: ".USER_PROFILE_ACTIVITY_BUTTONS),",218
replacement: {219
match: /.getId\(\)===\i.id/,220
replace: "$& && false"221
}222
}223
],224
225
settingsAboutComponent: () => {226
const [activity] = useAwaiter(createActivity, { fallbackValue: undefined, deps: Object.values(settings.store) });227
const gameActivityEnabled = ShowCurrentGame.useSetting();228
const { profileThemeStyle } = useProfileThemeStyle({});229
230
return (231
<>232
{!gameActivityEnabled && (233
<ErrorCard234
className={classes(Margins.top16, Margins.bottom16)}235
style={{ padding: "1em" }}236
>237
<Forms.FormTitle>Notice</Forms.FormTitle>238
<Forms.FormText>Activity Sharing isn039;t enabled, people won039;t be able to see your custom rich presence!</Forms.FormText>239
240
<Button241
color={Button.Colors.TRANSPARENT}242
className={Margins.top8}243
onClick={() => ShowCurrentGame.updateSetting(true)}244
>245
Enable246
</Button>247
</ErrorCard>248
)}249
250
<Flex flexDirection="column" gap=".5em" className={Margins.top16}>251
<Forms.FormText>252
Go to the <Link href="https:class="ts-cmt">//discord.com/developers/applications">Discord Developer Portal</Link> to create an application and253
get the application ID.254
</Forms.FormText>255
<Forms.FormText>256
Upload images in the Rich Presence tab to get the image keys.257
</Forms.FormText>258
<Forms.FormText>259
If you want to use an image link, download your image and reupload the image to <Link href="https:class="ts-cmt">//imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".260
</Forms.FormText>261
<Forms.FormText>262
You can039;t see your own buttons on your profile, but everyone else can see it fine.263
</Forms.FormText>264
<Forms.FormText>265
Some weird unicode text ("fonts" ๐๐๐๐ ๐๐๐๐) may cause the rich presence to not show up, try using normal letters instead.266
</Forms.FormText>267
</Flex>268
269
<Divider className={Margins.top8} />270
271
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-muted)" }}>272
{activity && <ActivityView273
activity={activity}274
user={UserStore.getCurrentUser()}275
currentUser={UserStore.getCurrentUser()}276
/>}277
</div>278
</>279
);280
}281
});282