aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/plugins/customRPC.tsx251
-rw-r--r--src/utils/constants.ts4
2 files changed, 255 insertions, 0 deletions
diff --git a/src/plugins/customRPC.tsx b/src/plugins/customRPC.tsx
new file mode 100644
index 0000000..9a0901b
--- /dev/null
+++ b/src/plugins/customRPC.tsx
@@ -0,0 +1,251 @@
+/*
+ * 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 { Link } from "@components/Link";
+import { Devs } from "@utils/constants";
+import { useAwaiter } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
+import {
+ FluxDispatcher,
+ Forms,
+ GuildStore,
+ React,
+ SelectedChannelStore,
+ SelectedGuildStore,
+ UserStore
+} from "@webpack/common";
+
+const ActivityComponent = findByCodeLazy("onOpenGameProfile");
+const ActivityClassName = findByPropsLazy("activity", "buttonColor");
+const Colors = findByPropsLazy("profileColors");
+
+// START yoinked from lastfm.tsx
+const assetManager = mapMangledModuleLazy(
+ "getAssetImage: size must === [number, number] for Twitch",
+ {
+ getAsset: filters.byCode("apply("),
+ }
+);
+
+async function getApplicationAsset(key: string): Promise<string> {
+ return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
+}
+
+interface ActivityAssets {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+}
+
+interface Activity {
+ state: string;
+ details?: string;
+ timestamps?: {
+ start?: Number;
+ end?: Number;
+ };
+ assets?: ActivityAssets;
+ buttons?: Array<string>;
+ name: string;
+ application_id: string;
+ metadata?: {
+ button_urls?: Array<string>;
+ };
+ type: ActivityType;
+ flags: Number;
+}
+
+enum ActivityType {
+ PLAYING = 0,
+ LISTENING = 2,
+ WATCHING = 3,
+ COMPETING = 5
+}
+// END
+
+const strOpt = (description: string) => ({
+ type: OptionType.STRING,
+ description,
+ onChange: setRpc
+}) as const;
+
+const numOpt = (description: string) => ({
+ type: OptionType.NUMBER,
+ description,
+ onChange: setRpc
+}) as const;
+
+const choice = (label: string, value: any, _default?: Boolean) => ({
+ label,
+ value,
+ default: _default
+}) as const;
+
+const choiceOpt = (description: string, options) => ({
+ type: OptionType.SELECT,
+ description,
+ onChange: setRpc,
+ options
+}) as const;
+
+
+const settings = definePluginSettings({
+ appID: strOpt("The ID of the application for the rich presence."),
+ appName: strOpt("The name of the presence."),
+ details: strOpt("Line 1 of rich presence."),
+ state: strOpt("Line 2 of rich presence."),
+ type: choiceOpt("Type of presence", [
+ choice("Playing", ActivityType.PLAYING, true),
+ choice("Listening", ActivityType.LISTENING),
+ choice("Watching", ActivityType.WATCHING),
+ choice("Competing", ActivityType.COMPETING)
+ ]),
+ startTime: numOpt("Unix Timestamp for beginning of activity."),
+ endTime: numOpt("Unix Timestamp for end of activity."),
+ imageBig: strOpt("Sets the big image to the specified image."),
+ imageBigTooltip: strOpt("Sets the tooltip text for the big image."),
+ imageSmall: strOpt("Sets the small image to the specified image."),
+ imageSmallTooltip: strOpt("Sets the tooltip text for the small image."),
+ buttonOneText: strOpt("The text for the first button"),
+ buttonOneURL: strOpt("The URL for the first button"),
+ buttonTwoText: strOpt("The text for the second button"),
+ buttonTwoURL: strOpt("The URL for the second button")
+});
+
+async function createActivity(): Promise<Activity | undefined> {
+ const {
+ appID,
+ appName,
+ details,
+ state,
+ type,
+ startTime,
+ endTime,
+ imageBig,
+ imageBigTooltip,
+ imageSmall,
+ imageSmallTooltip,
+ buttonOneText,
+ buttonOneURL,
+ buttonTwoText,
+ buttonTwoURL
+ } = settings.store;
+
+ if (!appName) return;
+
+ const activity: Activity = {
+ application_id: appID || "0",
+ name: appName,
+ state,
+ details,
+ type,
+ flags: 1 << 0,
+ };
+
+ if (startTime) {
+ activity.timestamps = {
+ start: startTime,
+ };
+ if (endTime) {
+ activity.timestamps.end = endTime;
+ }
+ }
+
+ if (buttonOneText) {
+ activity.buttons = [
+ buttonOneText,
+ buttonTwoText
+ ].filter(Boolean);
+
+ activity.metadata = {
+ button_urls: [
+ buttonOneURL,
+ buttonTwoURL
+ ].filter(Boolean)
+ };
+ }
+
+ if (imageBig) {
+ activity.assets = {
+ large_image: await getApplicationAsset(imageBig),
+ large_text: imageBigTooltip
+ };
+ }
+
+ if (imageSmall) {
+ activity.assets = {
+ ...activity.assets,
+ small_image: await getApplicationAsset(imageSmall),
+ small_text: imageSmallTooltip
+ };
+ }
+
+
+ for (const k in activity) {
+ if (k === "type") continue; // without type, the presence is considered invalid.
+ const v = activity[k];
+ if (!v || v.length === 0)
+ delete activity[k];
+ }
+
+ // WHAT DO YOU WANT FROM ME
+ // eslint-disable-next-line consistent-return
+ return activity;
+}
+
+async function setRpc(disable?: Boolean) {
+ const activity: Activity | undefined = await createActivity();
+
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity: !disable ? activity : {}
+ });
+}
+
+export default definePlugin({
+ name: "CustomRPC",
+ description: "Allows you to set a custom rich presence.",
+ authors: [Devs.captain],
+ start: setRpc,
+ stop: () => setRpc(true),
+ settings,
+
+ settingsAboutComponent: () => {
+ const activity = useAwaiter(createActivity);
+ return (
+ <>
+ <Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle>
+ <Forms.FormText>
+ You will need to <Link href="https://discord.com/developers/applications">create an
+ application</Link> and
+ get its ID to use this plugin.
+ </Forms.FormText>
+ <Forms.FormDivider />
+ <div style={{ width: "284px" }} className={Colors.profileColors}>
+ {activity[0] && <ActivityComponent activity={activity[0]} className={ActivityClassName.activity} channelId={SelectedChannelStore.getChannelId()}
+ guild={GuildStore.getGuild(SelectedGuildStore.getLastSelectedGuildId())}
+ application={{ id: settings.store.appID }}
+ user={UserStore.getCurrentUser()} />}
+ </div>
+ </>
+ );
+ }
+});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index b5e1eb6..1eba713 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -188,5 +188,9 @@ export const Devs = /* #__PURE__*/ Object.freeze({
zt: {
name: "zt",
id: 289556910426816513n
+ },
+ captain: {
+ name: "Captain",
+ id: 347366054806159360n
}
});