aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAutumnVN <autumnvnchino@gmail.com>2023-08-01 10:27:35 +0700
committerV <vendicated@riseup.net>2023-08-02 01:52:08 +0200
commit2e002107a6739c025ee6f916acaa76a3f3373bbc (patch)
tree2785dcd3efd7d6de5931687d042a8a28e21ca5ec
parentcc07518a34e62844fed57a408a6af1702e628200 (diff)
downloadVencord-2e002107a6739c025ee6f916acaa76a3f3373bbc.tar.gz
Vencord-2e002107a6739c025ee6f916acaa76a3f3373bbc.tar.bz2
Vencord-2e002107a6739c025ee6f916acaa76a3f3373bbc.zip
customRPC: add validation & some fixes (#1481)
Signed-off-by: V <vendicated@riseup.net>
-rw-r--r--src/plugins/customRPC.tsx332
1 files changed, 261 insertions, 71 deletions
diff --git a/src/plugins/customRPC.tsx b/src/plugins/customRPC.tsx
index 8d1be96..5a8a9aa 100644
--- a/src/plugins/customRPC.tsx
+++ b/src/plugins/customRPC.tsx
@@ -23,21 +23,12 @@ import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
-import {
- FluxDispatcher,
- Forms,
- GuildStore,
- React,
- SelectedChannelStore,
- SelectedGuildStore,
- UserStore
-} from "@webpack/common";
+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",
{
@@ -46,6 +37,7 @@ const assetManager = mapMangledModuleLazy(
);
async function getApplicationAsset(key: string): Promise<string> {
+ if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
}
@@ -71,66 +63,240 @@ interface Activity {
button_urls?: Array<string>;
};
type: ActivityType;
+ url?: string;
flags: number;
}
const enum ActivityType {
PLAYING = 0,
+ STREAMING = 1,
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 = <T,>(description: string, options: T) => ({
- type: OptionType.SELECT,
- description,
- onChange: setRpc,
- options
-}) as const;
+const enum TimestampMode {
+ NONE,
+ NOW,
+ TIME,
+ CUSTOM,
+}
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")
+ appID: {
+ type: OptionType.STRING,
+ description: "Application ID (required)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (!value) return "Application ID is required.";
+ if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
+ return true;
+ }
+ },
+ appName: {
+ type: OptionType.STRING,
+ description: "Application name (required)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (!value) return "Application name is required.";
+ if (value.length > 128) return "Application name must be not longer than 128 characters.";
+ return true;
+ }
+ },
+ details: {
+ type: OptionType.STRING,
+ description: "Details (line 1)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 128) return "Details (line 1) must be not longer than 128 characters.";
+ return true;
+ }
+ },
+ state: {
+ type: OptionType.STRING,
+ description: "State (line 2)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 128) return "State (line 2) must be not longer than 128 characters.";
+ return true;
+ }
+ },
+ type: {
+ type: OptionType.SELECT,
+ description: "Activity type",
+ restartNeeded: true,
+ onChange: setRpc,
+ options: [
+ {
+ label: "Playing",
+ value: ActivityType.PLAYING,
+ default: true
+ },
+ {
+ label: "Streaming",
+ value: ActivityType.STREAMING
+ },
+ {
+ label: "Listening",
+ value: ActivityType.LISTENING
+ },
+ {
+ label: "Watching",
+ value: ActivityType.WATCHING
+ },
+ {
+ label: "Competing",
+ value: ActivityType.COMPETING
+ }
+ ]
+ },
+ streamLink: {
+ type: OptionType.STRING,
+ description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isDisabled: isStreamLinkDisabled,
+ isValid: isStreamLinkValid
+ },
+ timestampMode: {
+ type: OptionType.SELECT,
+ description: "Timestamp mode",
+ restartNeeded: true,
+ onChange: setRpc,
+ options: [
+ {
+ label: "None",
+ value: TimestampMode.NONE,
+ default: true
+ },
+ {
+ label: "Since discord open",
+ value: TimestampMode.NOW
+ },
+ {
+ label: "Same as your current time",
+ value: TimestampMode.TIME
+ },
+ {
+ label: "Custom",
+ value: TimestampMode.CUSTOM
+ }
+ ]
+ },
+ startTime: {
+ type: OptionType.NUMBER,
+ description: "Start timestamp (only for custom timestamp mode)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isDisabled: isTimestampDisabled,
+ isValid: (value: number) => {
+ if (value && value < 0) return "Start timestamp must be greater than 0.";
+ return true;
+ }
+ },
+ endTime: {
+ type: OptionType.NUMBER,
+ description: "End timestamp (only for custom timestamp mode)",
+ restartNeeded: true,
+ onChange: setRpc,
+ isDisabled: isTimestampDisabled,
+ isValid: (value: number) => {
+ if (value && value < 0) return "End timestamp must be greater than 0.";
+ return true;
+ }
+ },
+ imageBig: {
+ type: OptionType.STRING,
+ description: "Big image key",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: isImageKeyValid
+ },
+ imageBigTooltip: {
+ type: OptionType.STRING,
+ description: "Big image tooltip",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 128) return "Big image tooltip must be not longer than 128 characters.";
+ return true;
+ }
+ },
+ imageSmall: {
+ type: OptionType.STRING,
+ description: "Small image key",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: isImageKeyValid
+ },
+ imageSmallTooltip: {
+ type: OptionType.STRING,
+ description: "Small image tooltip",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 128) return "Small image tooltip must be not longer than 128 characters.";
+ return true;
+ }
+ },
+ buttonOneText: {
+ type: OptionType.STRING,
+ description: "Button 1 text",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 31) return "Button 1 text must be not longer than 31 characters.";
+ return true;
+ }
+ },
+ buttonOneURL: {
+ type: OptionType.STRING,
+ description: "Button 1 URL",
+ restartNeeded: true,
+ onChange: setRpc
+ },
+ buttonTwoText: {
+ type: OptionType.STRING,
+ description: "Button 2 text",
+ restartNeeded: true,
+ onChange: setRpc,
+ isValid: (value: string) => {
+ if (value && value.length > 31) return "Button 2 text must be not longer than 31 characters.";
+ return true;
+ }
+ },
+ buttonTwoURL: {
+ type: OptionType.STRING,
+ description: "Button 2 URL",
+ restartNeeded: true,
+ onChange: setRpc
+ }
});
+function isStreamLinkDisabled(): boolean {
+ return settings.store.type !== ActivityType.STREAMING;
+}
+
+function isStreamLinkValid(): boolean | string {
+ if (settings.store.type === ActivityType.STREAMING && settings.store.streamLink && !/(https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+)/.test(settings.store.streamLink)) return "Streaming link must be a valid URL.";
+ return true;
+}
+
+function isTimestampDisabled(): boolean {
+ return settings.store.timestampMode !== TimestampMode.CUSTOM;
+}
+
+function isImageKeyValid(value: string) {
+ if (!/https?:\/\//.test(value)) return true;
+ if (/https?:\/\/(?!i\.)?imgur\.com\//.test(value)) return "Imgur link must be a direct link to the image. (e.g. https://i.imgur.com/...)";
+ if (/https?:\/\/(?!media\.)?tenor\.com\//.test(value)) return "Tenor link must be a direct link to the image. (e.g. https://media.tenor.com/...)";
+ return true;
+}
+
async function createActivity(): Promise<Activity | undefined> {
const {
appID,
@@ -138,6 +304,7 @@ async function createActivity(): Promise<Activity | undefined> {
details,
state,
type,
+ streamLink,
startTime,
endTime,
imageBig,
@@ -161,13 +328,32 @@ async function createActivity(): Promise<Activity | undefined> {
flags: 1 << 0,
};
- if (startTime) {
- activity.timestamps = {
- start: startTime,
- };
- if (endTime) {
- activity.timestamps.end = endTime;
- }
+ if (type === ActivityType.STREAMING) activity.url = streamLink;
+
+ switch (settings.store.timestampMode) {
+ case TimestampMode.NOW:
+ activity.timestamps = {
+ start: Math.floor(Date.now() / 1000)
+ };
+ break;
+ case TimestampMode.TIME:
+ activity.timestamps = {
+ start: Math.floor(Date.now() / 1000) - (new Date().getHours() * 3600) - (new Date().getMinutes() * 60) - new Date().getSeconds()
+ };
+ break;
+ case TimestampMode.CUSTOM:
+ if (startTime) {
+ activity.timestamps = {
+ start: startTime,
+ };
+ if (endTime) {
+ activity.timestamps.end = endTime;
+ }
+ }
+ break;
+ case TimestampMode.NONE:
+ default:
+ break;
}
if (buttonOneText) {
@@ -187,7 +373,7 @@ async function createActivity(): Promise<Activity | undefined> {
if (imageBig) {
activity.assets = {
large_image: await getApplicationAsset(imageBig),
- large_text: imageBigTooltip
+ large_text: imageBigTooltip || undefined
};
}
@@ -195,13 +381,13 @@ async function createActivity(): Promise<Activity | undefined> {
activity.assets = {
...activity.assets,
small_image: await getApplicationAsset(imageSmall),
- small_text: imageSmallTooltip
+ small_text: imageSmallTooltip || undefined
};
}
for (const k in activity) {
- if (k === "type") continue; // without type, the presence is considered invalid.
+ if (k === "type") continue;
const v = activity[k];
if (!v || v.length === 0)
delete activity[k];
@@ -223,7 +409,7 @@ async function setRpc(disable?: boolean) {
export default definePlugin({
name: "CustomRPC",
description: "Allows you to set a custom rich presence.",
- authors: [Devs.captain],
+ authors: [Devs.captain, Devs.AutumnVN],
start: setRpc,
stop: () => setRpc(true),
settings,
@@ -232,11 +418,15 @@ export default definePlugin({
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.
+ Go to <Link href="https://discord.com/developers/applications">Discord Deverloper Portal</Link> to create an application and
+ get the application ID.
+ </Forms.FormText>
+ <Forms.FormText>
+ Upload images in the Rich Presence tab to get the image keys.
+ </Forms.FormText>
+ <Forms.FormText>
+ If you want to use 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 select "Copy image address".
</Forms.FormText>
<Forms.FormDivider />
<div style={{ width: "284px" }} className={Colors.profileColors}>