Plugin

CustomRPC

Add a fully customisable Rich Presence (Game status) to your Discord profile

Activity Customisation
index.tsx
Download

Source

src/plugins/customRPC/index.tsx
1/*
2 * FiveCord โ€” a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { definePluginSettings } from "@api/Settings";
8import { getUserSettingLazy } from "@api/UserSettings";
9import { Divider } from "@components/Divider";
10import { ErrorCard } from "@components/ErrorCard";
11import { Flex } from "@components/Flex";
12import { Link } from "@components/Link";
13import { Devs } from "@utils/constants";
14import { isTruthy } from "@utils/guards";
15import { Margins } from "@utils/margins";
16import { classes } from "@utils/misc";
17import { useAwaiter } from "@utils/react";
18import definePlugin, { OptionType } from "@utils/types";
19import { Activity } from "@vencord/discord-types";
20import { ActivityType } from "@vencord/discord-types/enums";
21import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
22import { ApplicationAssetUtils, Button, FluxDispatcher, Forms, React, UserStore } from "@webpack/common";
23
24import { RPCSettings } from "./RpcSettings";
25
26const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
27const ActivityView = findComponentByCodeLazy(".party?(0", "USER_PROFILE_ACTIVITY");
28
29const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
30
31async function getApplicationAsset(key: string): Promise<string> {
32 return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
33}
34
35export const enum TimestampMode {
36 NONE,
37 NOW,
38 TIME,
39 CUSTOM,
40}
41
42export const settings = definePluginSettings({
43 config: {
44 type: OptionType.COMPONENT,
45 component: RPCSettings
46 },
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
73async 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 timestampMode
98 } = 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()) * 1000
122 };
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 buttonTwoText
148 ].filter(isTruthy);
149
150 activity.metadata = {
151 button_urls: [
152 buttonOneURL,
153 buttonTwoURL
154 ].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 || undefined
163 };
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 || undefined
172 };
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
191export 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
201export 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 restart
208 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 behaviour
215 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 <ErrorCard
234 className={classes(Margins.top16, Margins.bottom16)}
235 style={{ padding: "1em" }}
236 >
237 <Forms.FormTitle>Notice</Forms.FormTitle>
238 <Forms.FormText>Activity Sharing isn&#039;t enabled, people won&#039;t be able to see your custom rich presence!</Forms.FormText>
239
240 <Button
241 color={Button.Colors.TRANSPARENT}
242 className={Margins.top8}
243 onClick={() => ShowCurrentGame.updateSetting(true)}
244 >
245 Enable
246 </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 and
253 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 can&#039;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 && <ActivityView
273 activity={activity}
274 user={UserStore.getCurrentUser()}
275 currentUser={UserStore.getCurrentUser()}
276 />}
277 </div>
278 </>
279 );
280 }
281});
282