Equicord/src/plugins/customRPC/index.tsx

295 lines
9.5 KiB
TypeScript
Raw Normal View History

/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { definePluginSettings } from "@api/Settings";
import { getUserSettingLazy } from "@api/UserSettings";
import { Divider } from "@components/Divider";
import { ErrorCard } from "@components/ErrorCard";
import { Flex } from "@components/Flex";
2025-10-10 21:11:31 -04:00
import { Heading } from "@components/Heading";
import { Link } from "@components/Link";
2025-10-11 21:50:14 -04:00
import { Paragraph } from "@components/Paragraph";
import { Devs } from "@utils/constants";
import { isTruthy } from "@utils/guards";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { Activity } from "@vencord/discord-types";
import { ActivityType } from "@vencord/discord-types/enums";
import { findByCodeLazy, findComponentByCodeLazy } from "@webpack";
2025-10-11 21:50:14 -04:00
import { ApplicationAssetUtils, Button, FluxDispatcher, React, UserStore } from "@webpack/common";
import { RPCSettings } from "./RpcSettings";
2024-04-18 00:40:09 +02:00
const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gradient-primary-color");
2026-01-23 13:02:50 -05:00
const ActivityView = findComponentByCodeLazy(".party?(0", "USER_PROFILE_ACTIVITY");
const ShowCurrentGame = getUserSettingLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> {
2023-10-25 13:20:32 -03:00
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];
}
export const enum TimestampMode {
NONE,
NOW,
TIME,
CUSTOM,
}
export const settings = definePluginSettings({
config: {
type: OptionType.COMPONENT,
component: RPCSettings
},
}).withPrivateSettings<{
appID?: string;
appName?: string;
details?: string;
detailsURL?: string;
state?: string;
stateURL?: string;
type?: ActivityType;
streamLink?: string;
timestampMode?: TimestampMode;
startTime?: number;
endTime?: number;
imageBig?: string;
imageBigURL?: string;
imageBigTooltip?: string;
imageSmall?: string;
imageSmallURL?: string;
imageSmallTooltip?: string;
buttonOneText?: string;
buttonOneURL?: string;
buttonTwoText?: string;
buttonTwoURL?: string;
partySize?: number;
partyMaxSize?: number;
}>();
async function createActivity(): Promise<Activity | undefined> {
const {
appID,
appName,
details,
detailsURL,
state,
stateURL,
type,
streamLink,
startTime,
endTime,
imageBig,
imageBigURL,
imageBigTooltip,
imageSmall,
imageSmallURL,
imageSmallTooltip,
buttonOneText,
buttonOneURL,
buttonTwoText,
buttonTwoURL,
partyMaxSize,
partySize,
timestampMode
} = settings.store;
if (!appName) return;
const activity: Activity = {
application_id: appID || "0",
name: appName,
state,
details,
type: type ?? ActivityType.PLAYING,
flags: 1 << 0,
};
if (type === ActivityType.STREAMING) activity.url = streamLink;
switch (timestampMode) {
case TimestampMode.NOW:
activity.timestamps = {
start: Date.now()
};
break;
case TimestampMode.TIME:
activity.timestamps = {
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000
};
break;
case TimestampMode.CUSTOM:
if (startTime || endTime) {
activity.timestamps = {};
if (startTime) activity.timestamps.start = startTime;
if (endTime) activity.timestamps.end = endTime;
}
break;
case TimestampMode.NONE:
default:
break;
}
if (detailsURL) {
activity.details_url = detailsURL;
}
if (stateURL) {
activity.state_url = stateURL;
}
if (buttonOneText) {
activity.buttons = [
buttonOneText,
buttonTwoText
].filter(isTruthy);
activity.metadata = {
button_urls: [
buttonOneURL,
buttonTwoURL
].filter(isTruthy)
};
}
if (imageBig) {
activity.assets = {
large_image: await getApplicationAsset(imageBig),
large_text: imageBigTooltip || undefined,
large_url: imageBigURL || undefined
};
}
if (imageSmall) {
activity.assets = {
...activity.assets,
small_image: await getApplicationAsset(imageSmall),
small_text: imageSmallTooltip || undefined,
small_url: imageSmallURL || undefined
};
}
if (partyMaxSize && partySize) {
activity.party = {
size: [partySize, partyMaxSize]
};
}
for (const k in activity) {
if (k === "type") continue;
const v = activity[k];
if (!v || v.length === 0)
delete activity[k];
}
return activity;
}
export async function setRpc(disable?: boolean) {
const activity: Activity | undefined = await createActivity();
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: !disable ? activity : null,
socketId: "CustomRPC",
});
}
export default definePlugin({
name: "CustomRPC",
description: "Add a fully customisable Rich Presence (Game status) to your Discord profile",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["UserSettingsAPI"],
// This plugin's patch is not important for functionality, so don't require a restart
requiresRestart: false,
settings,
start: setRpc,
stop: () => setRpc(true),
// Discord hides buttons on your own Rich Presence for some reason. This patch disables that behaviour
2025-01-22 22:22:43 -03:00
patches: [
{
2026-02-04 23:00:28 -05:00
find: ".USER_PROFILE_ACTIVITY_BUTTONS),",
2025-01-22 22:22:43 -03:00
replacement: {
match: /.getId\(\)===\i.id/,
replace: "$& && false"
},
2025-01-22 22:22:43 -03:00
}
],
settingsAboutComponent: () => {
const [activity] = useAwaiter(createActivity, { fallbackValue: undefined, deps: Object.values(settings.store) });
const gameActivityEnabled = ShowCurrentGame.useSetting();
2024-04-18 00:40:09 +02:00
const { profileThemeStyle } = useProfileThemeStyle({});
return (
<>
{!gameActivityEnabled && (
<ErrorCard
className={classes(Margins.top16, Margins.bottom16)}
style={{ padding: "1em" }}
>
2025-10-10 21:11:31 -04:00
<Heading>Notice</Heading>
2025-10-11 21:50:14 -04:00
<Paragraph>Activity Sharing isn't enabled, people won't be able to see your custom rich presence!</Paragraph>
<Button
color={Button.Colors.TRANSPARENT}
className={Margins.top8}
onClick={() => ShowCurrentGame.updateSetting(true)}
>
Enable
</Button>
</ErrorCard>
)}
<Flex flexDirection="column" gap=".5em" className={Margins.top16}>
2025-11-25 23:07:57 -05:00
<Paragraph>
Go to the <Link href="https://discord.com/developers/applications">Discord Developer Portal</Link> to create an application and
get the application ID.
2025-10-11 21:50:14 -04:00
</Paragraph>
<Paragraph>
Upload images in the Rich Presence tab to get the image keys.
2025-10-11 21:50:14 -04:00
</Paragraph>
<Paragraph>
If you want to use an image link, download your image and reupload the image to <Link href="https://imgur.com">Imgur</Link> and get the image link by right-clicking the image and selecting "Copy image address".
2025-10-11 21:50:14 -04:00
</Paragraph>
<Paragraph>
You can't see your own buttons on your profile, but everyone else can see it fine.
2025-10-11 21:50:14 -04:00
</Paragraph>
<Paragraph>
Some weird unicode text ("fonts" 𝖑𝖎𝖐𝖊 𝖙𝖍𝖎𝖘) may cause the rich presence to not show up, try using normal letters instead.
2025-10-11 21:50:14 -04:00
</Paragraph>
</Flex>
<Divider className={Margins.top8} />
<div style={{ width: "284px", ...profileThemeStyle, marginTop: 8, borderRadius: 8, background: "var(--background-mod-muted)" }}>
{activity && <ActivityView
activity={activity}
user={UserStore.getCurrentUser()}
2025-01-22 22:22:43 -03:00
currentUser={UserStore.getCurrentUser()}
/>}
</div>
</>
);
}
});