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