customRPC: add validation & some fixes (#1481)

Signed-off-by: V <vendicated@riseup.net>
This commit is contained in:
AutumnVN 2023-08-01 10:27:35 +07:00 committed by V
parent cc07518a34
commit 2e002107a6
No known key found for this signature in database
GPG Key ID: A1DC0CFB5615D905

@ -23,21 +23,12 @@ import { isTruthy } from "@utils/guards";
import { useAwaiter } from "@utils/react"; import { useAwaiter } from "@utils/react";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack"; import { filters, findByCodeLazy, findByPropsLazy, mapMangledModuleLazy } from "@webpack";
import { import { FluxDispatcher, Forms, GuildStore, React, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";
FluxDispatcher,
Forms,
GuildStore,
React,
SelectedChannelStore,
SelectedGuildStore,
UserStore
} from "@webpack/common";
const ActivityComponent = findByCodeLazy("onOpenGameProfile"); const ActivityComponent = findByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor"); const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const Colors = findByPropsLazy("profileColors"); const Colors = findByPropsLazy("profileColors");
// START yoinked from lastfm.tsx
const assetManager = mapMangledModuleLazy( const assetManager = mapMangledModuleLazy(
"getAssetImage: size must === [number, number] for Twitch", "getAssetImage: size must === [number, number] for Twitch",
{ {
@ -46,6 +37,7 @@ const assetManager = mapMangledModuleLazy(
); );
async function getApplicationAsset(key: string): Promise<string> { 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]; return (await assetManager.getAsset(settings.store.appID, [key, undefined]))[0];
} }
@ -71,66 +63,240 @@ interface Activity {
button_urls?: Array<string>; button_urls?: Array<string>;
}; };
type: ActivityType; type: ActivityType;
url?: string;
flags: number; flags: number;
} }
const enum ActivityType { const enum ActivityType {
PLAYING = 0, PLAYING = 0,
STREAMING = 1,
LISTENING = 2, LISTENING = 2,
WATCHING = 3, WATCHING = 3,
COMPETING = 5 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({ const settings = definePluginSettings({
appID: strOpt("The ID of the application for the rich presence."), appID: {
appName: strOpt("The name of the presence."), type: OptionType.STRING,
details: strOpt("Line 1 of rich presence."), description: "Application ID (required)",
state: strOpt("Line 2 of rich presence."), restartNeeded: true,
type: choiceOpt("Type of presence", [ onChange: setRpc,
choice("Playing", ActivityType.PLAYING, true), isValid: (value: string) => {
choice("Listening", ActivityType.LISTENING), if (!value) return "Application ID is required.";
choice("Watching", ActivityType.WATCHING), if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";
choice("Competing", ActivityType.COMPETING) return true;
]), }
startTime: numOpt("Unix Timestamp for beginning of activity."), },
endTime: numOpt("Unix Timestamp for end of activity."), appName: {
imageBig: strOpt("Sets the big image to the specified image."), type: OptionType.STRING,
imageBigTooltip: strOpt("Sets the tooltip text for the big image."), description: "Application name (required)",
imageSmall: strOpt("Sets the small image to the specified image."), restartNeeded: true,
imageSmallTooltip: strOpt("Sets the tooltip text for the small image."), onChange: setRpc,
buttonOneText: strOpt("The text for the first button"), isValid: (value: string) => {
buttonOneURL: strOpt("The URL for the first button"), if (!value) return "Application name is required.";
buttonTwoText: strOpt("The text for the second button"), if (value.length > 128) return "Application name must be not longer than 128 characters.";
buttonTwoURL: strOpt("The URL for the second button") 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> { async function createActivity(): Promise<Activity | undefined> {
const { const {
appID, appID,
@ -138,6 +304,7 @@ async function createActivity(): Promise<Activity | undefined> {
details, details,
state, state,
type, type,
streamLink,
startTime, startTime,
endTime, endTime,
imageBig, imageBig,
@ -161,13 +328,32 @@ async function createActivity(): Promise<Activity | undefined> {
flags: 1 << 0, flags: 1 << 0,
}; };
if (startTime) { if (type === ActivityType.STREAMING) activity.url = streamLink;
activity.timestamps = {
start: startTime, switch (settings.store.timestampMode) {
}; case TimestampMode.NOW:
if (endTime) { activity.timestamps = {
activity.timestamps.end = endTime; 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) { if (buttonOneText) {
@ -187,7 +373,7 @@ async function createActivity(): Promise<Activity | undefined> {
if (imageBig) { if (imageBig) {
activity.assets = { activity.assets = {
large_image: await getApplicationAsset(imageBig), 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 = {
...activity.assets, ...activity.assets,
small_image: await getApplicationAsset(imageSmall), small_image: await getApplicationAsset(imageSmall),
small_text: imageSmallTooltip small_text: imageSmallTooltip || undefined
}; };
} }
for (const k in activity) { for (const k in activity) {
if (k === "type") continue; // without type, the presence is considered invalid. if (k === "type") continue;
const v = activity[k]; const v = activity[k];
if (!v || v.length === 0) if (!v || v.length === 0)
delete activity[k]; delete activity[k];
@ -223,7 +409,7 @@ async function setRpc(disable?: boolean) {
export default definePlugin({ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Allows you to set a custom rich presence.",
authors: [Devs.captain], authors: [Devs.captain, Devs.AutumnVN],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,
@ -232,11 +418,15 @@ export default definePlugin({
const activity = useAwaiter(createActivity); const activity = useAwaiter(createActivity);
return ( return (
<> <>
<Forms.FormTitle tag="h2">NOTE:</Forms.FormTitle>
<Forms.FormText> <Forms.FormText>
You will need to <Link href="https://discord.com/developers/applications">create an Go to <Link href="https://discord.com/developers/applications">Discord Deverloper Portal</Link> to create an application and
application</Link> and get the application ID.
get its ID to use this plugin. </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.FormText>
<Forms.FormDivider /> <Forms.FormDivider />
<div style={{ width: "284px" }} className={Colors.profileColors}> <div style={{ width: "284px" }} className={Colors.profileColors}>