2023-01-18 23:47:40 +01:00
/ *
* 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/>.
* /
2023-08-25 14:30:12 +02:00
import { definePluginSettings , Settings } from "@api/Settings" ;
2024-06-20 04:49:42 +02:00
import { getUserSettingLazy } from "@api/UserSettings" ;
2024-05-11 23:29:31 +02:00
import { ErrorCard } from "@components/ErrorCard" ;
2025-01-23 01:33:11 +01:00
import { Flex } from "@components/Flex" ;
2023-01-18 23:47:40 +01:00
import { Link } from "@components/Link" ;
import { Devs } from "@utils/constants" ;
2023-02-28 06:12:35 +01:00
import { isTruthy } from "@utils/guards" ;
2024-05-11 23:29:31 +02:00
import { Margins } from "@utils/margins" ;
import { classes } from "@utils/misc" ;
2023-11-22 07:04:17 +01:00
import { useAwaiter } from "@utils/react" ;
2023-01-18 23:47:40 +01:00
import definePlugin , { OptionType } from "@utils/types" ;
2024-09-06 14:20:15 +02:00
import { findByCodeLazy , findComponentByCodeLazy } from "@webpack" ;
2025-01-23 01:33:11 +01:00
import { ApplicationAssetUtils , Button , FluxDispatcher , Forms , React , UserStore } from "@webpack/common" ;
2023-01-18 23:47:40 +01:00
2024-04-18 00:40:09 +02:00
const useProfileThemeStyle = findByCodeLazy ( "profileThemeStyle:" , "--profile-gradient-primary-color" ) ;
2025-01-23 02:22:43 +01:00
const ActivityView = findComponentByCodeLazy ( ".party?(0" , ".card" ) ;
2023-01-18 23:47:40 +01:00
2024-06-20 04:49:42 +02:00
const ShowCurrentGame = getUserSettingLazy < boolean > ( "status" , "showCurrentGame" ) ! ;
2024-06-19 03:04:15 +02:00
2023-01-18 23:47:40 +01:00
async function getApplicationAsset ( key : string ) : Promise < string > {
2023-10-25 18:20:32 +02:00
return ( await ApplicationAssetUtils . fetchAssetIds ( settings . store . appID ! , [ key ] ) ) [ 0 ] ;
2023-01-18 23:47:40 +01:00
}
interface ActivityAssets {
large_image? : string ;
large_text? : string ;
small_image? : string ;
small_text? : string ;
}
interface Activity {
2023-02-28 06:12:35 +01:00
state? : string ;
2023-01-18 23:47:40 +01:00
details? : string ;
timestamps ? : {
2023-02-28 06:12:35 +01:00
start? : number ;
end? : number ;
2023-01-18 23:47:40 +01:00
} ;
assets? : ActivityAssets ;
buttons? : Array < string > ;
name : string ;
application_id : string ;
metadata ? : {
button_urls? : Array < string > ;
} ;
type : ActivityType ;
2023-08-01 05:27:35 +02:00
url? : string ;
2023-02-28 06:12:35 +01:00
flags : number ;
2023-01-18 23:47:40 +01:00
}
2023-06-13 02:36:25 +02:00
const enum ActivityType {
2023-01-18 23:47:40 +01:00
PLAYING = 0 ,
2023-08-01 05:27:35 +02:00
STREAMING = 1 ,
2023-01-18 23:47:40 +01:00
LISTENING = 2 ,
WATCHING = 3 ,
COMPETING = 5
}
2023-08-01 05:27:35 +02:00
const enum TimestampMode {
NONE ,
NOW ,
TIME ,
CUSTOM ,
}
2023-01-18 23:47:40 +01:00
const settings = definePluginSettings ( {
2023-08-01 05:27:35 +02:00
appID : {
type : OptionType . STRING ,
description : "Application ID (required)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
disabled : isStreamLinkDisabled ,
2023-08-01 05:27:35 +02:00
isValid : isStreamLinkValid
} ,
timestampMode : {
type : OptionType . SELECT ,
description : "Timestamp mode" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
options : [
{
label : "None" ,
value : TimestampMode.NONE ,
default : true
} ,
{
label : "Since discord open" ,
value : TimestampMode.NOW
} ,
{
2025-01-23 01:33:11 +01:00
label : "Same as your current time (not reset after 24h)" ,
2023-08-01 05:27:35 +02:00
value : TimestampMode.TIME
} ,
{
label : "Custom" ,
value : TimestampMode.CUSTOM
}
]
} ,
startTime : {
type : OptionType . NUMBER ,
2024-06-07 20:24:49 +02:00
description : "Start timestamp in milliseconds (only for custom timestamp mode)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
disabled : isTimestampDisabled ,
2023-08-01 05:27:35 +02:00
isValid : ( value : number ) = > {
if ( value && value < 0 ) return "Start timestamp must be greater than 0." ;
return true ;
}
} ,
endTime : {
type : OptionType . NUMBER ,
2024-06-07 20:24:49 +02:00
description : "End timestamp in milliseconds (only for custom timestamp mode)" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
disabled : isTimestampDisabled ,
2023-08-01 05:27:35 +02:00
isValid : ( value : number ) = > {
if ( value && value < 0 ) return "End timestamp must be greater than 0." ;
return true ;
}
} ,
imageBig : {
type : OptionType . STRING ,
2023-08-25 14:30:12 +02:00
description : "Big image key/link" ,
onChange : onChange ,
2023-08-01 05:27:35 +02:00
isValid : isImageKeyValid
} ,
imageBigTooltip : {
type : OptionType . STRING ,
description : "Big image tooltip" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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 ,
2023-08-25 14:30:12 +02:00
description : "Small image key/link" ,
onChange : onChange ,
2023-08-01 05:27:35 +02:00
isValid : isImageKeyValid
} ,
imageSmallTooltip : {
type : OptionType . STRING ,
description : "Small image tooltip" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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" ,
2023-08-25 14:30:12 +02:00
onChange : onChange
2023-08-01 05:27:35 +02:00
} ,
buttonTwoText : {
type : OptionType . STRING ,
description : "Button 2 text" ,
2023-08-25 14:30:12 +02:00
onChange : onChange ,
2023-08-01 05:27:35 +02:00
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" ,
2023-08-25 14:30:12 +02:00
onChange : onChange
2023-08-01 05:27:35 +02:00
}
2023-01-18 23:47:40 +01:00
} ) ;
2023-08-25 14:30:12 +02:00
function onChange() {
setRpc ( true ) ;
if ( Settings . plugins . CustomRPC . enabled ) setRpc ( ) ;
}
function isStreamLinkDisabled() {
2023-08-01 05:27:35 +02:00
return settings . store . type !== ActivityType . STREAMING ;
}
2023-08-25 14:30:12 +02:00
function isStreamLinkValid ( value : string ) {
if ( ! isStreamLinkDisabled ( ) && ! /https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/ . test ( value ) ) return "Streaming link must be a valid URL." ;
2025-01-23 01:33:11 +01:00
if ( value && value . length > 512 ) return "Streaming link must be not longer than 512 characters." ;
2023-08-01 05:27:35 +02:00
return true ;
}
2023-08-25 14:30:12 +02:00
function isTimestampDisabled() {
2023-08-01 05:27:35 +02:00
return settings . store . timestampMode !== TimestampMode . CUSTOM ;
}
function isImageKeyValid ( value : string ) {
2025-01-23 01:33:11 +01:00
if ( /https?:\/\/(cdn|media)\.discordapp\.(com|net)\// . test ( value ) ) return "Don't use a Discord link. Use an Imgur image link instead." ;
if ( /https?:\/\/(?!i\.)?imgur\.com\// . test ( value ) ) return "Imgur link must be a direct link to the image (e.g. https://i.imgur.com/...). Right click the image and click 'Copy image address'" ;
if ( /https?:\/\/(?!media\.)?tenor\.com\// . test ( value ) ) return "Tenor link must be a direct link to the image (e.g. https://media.tenor.com/...). Right click the GIF and click 'Copy image address'" ;
2023-08-01 05:27:35 +02:00
return true ;
}
2023-01-18 23:47:40 +01:00
async function createActivity ( ) : Promise < Activity | undefined > {
const {
appID ,
appName ,
details ,
state ,
type ,
2023-08-01 05:27:35 +02:00
streamLink ,
2023-01-18 23:47:40 +01:00
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 ,
} ;
2023-08-01 05:27:35 +02:00
if ( type === ActivityType . STREAMING ) activity . url = streamLink ;
switch ( settings . store . timestampMode ) {
case TimestampMode . NOW :
activity . timestamps = {
2024-03-07 13:51:14 +01:00
start : Date.now ( )
2023-08-01 05:27:35 +02:00
} ;
break ;
case TimestampMode . TIME :
activity . timestamps = {
2024-03-07 13:51:14 +01:00
start : Date.now ( ) - ( new Date ( ) . getHours ( ) * 3600 + new Date ( ) . getMinutes ( ) * 60 + new Date ( ) . getSeconds ( ) ) * 1000
2023-08-01 05:27:35 +02:00
} ;
break ;
case TimestampMode . CUSTOM :
2023-08-25 14:30:12 +02:00
if ( startTime || endTime ) {
activity . timestamps = { } ;
if ( startTime ) activity . timestamps . start = startTime ;
if ( endTime ) activity . timestamps . end = endTime ;
2023-08-01 05:27:35 +02:00
}
break ;
case TimestampMode . NONE :
default :
break ;
2023-01-18 23:47:40 +01:00
}
if ( buttonOneText ) {
activity . buttons = [
buttonOneText ,
buttonTwoText
2023-02-28 06:12:35 +01:00
] . filter ( isTruthy ) ;
2023-01-18 23:47:40 +01:00
activity . metadata = {
button_urls : [
buttonOneURL ,
buttonTwoURL
2023-02-28 06:12:35 +01:00
] . filter ( isTruthy )
2023-01-18 23:47:40 +01:00
} ;
}
if ( imageBig ) {
activity . assets = {
large_image : await getApplicationAsset ( imageBig ) ,
2023-08-01 05:27:35 +02:00
large_text : imageBigTooltip || undefined
2023-01-18 23:47:40 +01:00
} ;
}
if ( imageSmall ) {
activity . assets = {
. . . activity . assets ,
small_image : await getApplicationAsset ( imageSmall ) ,
2023-08-01 05:27:35 +02:00
small_text : imageSmallTooltip || undefined
2023-01-18 23:47:40 +01:00
} ;
}
for ( const k in activity ) {
2023-08-01 05:27:35 +02:00
if ( k === "type" ) continue ;
2023-01-18 23:47:40 +01:00
const v = activity [ k ] ;
if ( ! v || v . length === 0 )
delete activity [ k ] ;
}
return activity ;
}
2023-02-28 06:12:35 +01:00
async function setRpc ( disable? : boolean ) {
2023-01-18 23:47:40 +01:00
const activity : Activity | undefined = await createActivity ( ) ;
FluxDispatcher . dispatch ( {
type : "LOCAL_ACTIVITY_UPDATE" ,
2023-03-25 04:00:27 +01:00
activity : ! disable ? activity : null ,
socketId : "CustomRPC" ,
2023-01-18 23:47:40 +01:00
} ) ;
}
export default definePlugin ( {
name : "CustomRPC" ,
2025-01-23 01:33:11 +01:00
description : "Add a fully customisable Rich Presence (Game status) to your Discord profile" ,
2024-05-11 23:29:31 +02:00
authors : [ Devs . captain , Devs . AutumnVN , Devs . nin0dev ] ,
2024-06-20 04:49:42 +02:00
dependencies : [ "UserSettingsAPI" ] ,
2023-01-18 23:47:40 +01:00
start : setRpc ,
stop : ( ) = > setRpc ( true ) ,
settings ,
2025-01-23 02:22:43 +01:00
patches : [
{
find : ".party?(0" ,
all : true ,
replacement : {
match : /\i\.id===\i\.id\?null:/ ,
replace : ""
}
}
] ,
2023-01-18 23:47:40 +01:00
settingsAboutComponent : ( ) = > {
const activity = useAwaiter ( createActivity ) ;
2024-06-19 03:04:15 +02:00
const gameActivityEnabled = ShowCurrentGame . useSetting ( ) ;
2024-04-18 00:40:09 +02:00
const { profileThemeStyle } = useProfileThemeStyle ( { } ) ;
2023-01-18 23:47:40 +01:00
return (
< >
2024-05-11 23:29:31 +02:00
{ ! gameActivityEnabled && (
< ErrorCard
className = { classes ( Margins . top16 , Margins . bottom16 ) }
style = { { padding : "1em" } }
>
< Forms.FormTitle > Notice < / Forms.FormTitle >
2025-01-23 01:33:11 +01:00
< Forms.FormText > Activity Sharing isn 't enabled, people won' t be able to see your custom rich presence ! < / Forms.FormText >
2024-05-11 23:29:31 +02:00
< Button
color = { Button . Colors . TRANSPARENT }
className = { Margins . top8 }
2024-06-19 03:04:15 +02:00
onClick = { ( ) = > ShowCurrentGame . updateSetting ( true ) }
2024-05-11 23:29:31 +02:00
>
Enable
< / Button >
< / ErrorCard >
) }
2025-01-23 01:33:11 +01:00
< Flex flexDirection = "column" style = { { gap : ".5em" } } className = { Margins . top16 } >
< Forms.FormText >
Go to the < Link href = "https://discord.com/developers/applications" > Discord Developer 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 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" .
< / Forms.FormText >
< Forms.FormText >
You can ' t see your own buttons on your profile , but everyone else can see it fine .
< / Forms.FormText >
< Forms.FormText >
Some weird unicode text ( "fonts" 𝖑 𝖎 𝖐 𝖊 𝖙 𝖍 𝖎 𝖘 ) may cause the rich presence to not show up , try using normal letters instead .
< / Forms.FormText >
< / Flex >
2024-05-11 23:29:31 +02:00
< Forms.FormDivider className = { Margins . top8 } / >
2025-01-23 01:33:11 +01:00
< div style = { { width : "284px" , . . . profileThemeStyle , marginTop : 8 , borderRadius : 8 , background : "var(--bg-mod-faint)" } } >
{ activity [ 0 ] && < ActivityView
activity = { activity [ 0 ] }
user = { UserStore . getCurrentUser ( ) }
2025-01-23 02:22:43 +01:00
currentUser = { UserStore . getCurrentUser ( ) }
2025-01-23 01:33:11 +01:00
/ > }
2023-01-18 23:47:40 +01:00
< / div >
< / >
) ;
}
} ) ;