Plugin
FiveCordRPC
how to expose yourself
1
/*2
* FiveCord โ a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { definePluginSettings } from "@api/Settings";8
import { Devs } from "@utils/constants";9
import { isTruthy } from "@utils/guards";10
import definePlugin, { OptionType } from "@utils/types";11
import { findByPropsLazy, findStoreLazy } from "@webpack";12
import { ApplicationAssetUtils, ChannelStore, FluxDispatcher, GuildStore, PresenceStore, RelationshipStore, SelectedChannelStore, SelectedGuildStore, UserStore } from "@webpack/common";13
import { FluxStore } from "@webpack/types";14
import { Channel } from "discord-types/general";15
16
const presenceStore = findByPropsLazy("getLocalPresence");17
const GuildMemberCountStore = findStoreLazy("GuildMemberCountStore") as FluxStore & { getMemberCount(guildId: string): number | null; };18
const ChannelMemberStore = findStoreLazy("ChannelMemberStore") as FluxStore & {19
getProps(guildId: string, channelId: string): { groups: { count: number; id: string; }[]; };20
};21
const VoiceStates = findByPropsLazy("getVoiceStatesForChannel");22
const chino = "https:class="ts-cmt">//i.imgur.com/Dsa2rQy.png";23
const shiggy = "https:class="ts-cmt">//i.imgur.com/MgUzhs0.gif";24
const wysi = "https:class="ts-cmt">//i.imgur.com/uKtXde9.gif";25
26
async function getApplicationAsset(key: string): Promise<string> {27
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\class="ts-cmt">//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");28
return (await ApplicationAssetUtils.fetchAssetIds(settings.store.appID!, [key]))[0];29
}30
31
interface ActivityAssets {32
large_image?: string;33
large_text?: string;34
small_image?: string;35
small_text?: string;36
}37
38
interface Activity {39
state?: string;40
details?: string;41
timestamps?: {42
start?: number;43
end?: number;44
};45
assets?: ActivityAssets;46
buttons?: Array<string>;47
name: string;48
application_id: string;49
metadata?: {50
button_urls?: Array<string>;51
};52
type: ActivityType;53
url?: string;54
flags: number;55
}56
57
const enum ActivityType {58
PLAYING = 0,59
STREAMING = 1,60
LISTENING = 2,61
WATCHING = 3,62
COMPETING = 563
}64
65
const enum TimestampMode {66
NONE,67
NOW,68
TIME,69
CUSTOM,70
}71
72
const settings = definePluginSettings({73
appID: {74
type: OptionType.STRING,75
description: "Application ID (required)",76
onChange: onChange,77
isValid: (value: string) => {78
if (!value) return "Application ID is required.";79
if (value && !/^\d+$/.test(value)) return "Application ID must be a number.";80
return true;81
}82
},83
userAvatarAsSmallImage: {84
type: OptionType.BOOLEAN,85
description: "Use your avatar as small image",86
onChange: onChange,87
default: false88
},89
exposeDmsUsername: {90
type: OptionType.BOOLEAN,91
description: "Expose current DMs username",92
onChange: onChange,93
default: false94
},95
type: {96
type: OptionType.SELECT,97
description: "Activity type",98
onChange: onChange,99
options: [100
{101
label: "Playing",102
value: ActivityType.PLAYING,103
default: true104
},105
{106
label: "Streaming",107
value: ActivityType.STREAMING108
},109
{110
label: "Listening",111
value: ActivityType.LISTENING112
},113
{114
label: "Watching",115
value: ActivityType.WATCHING116
},117
{118
label: "Competing",119
value: ActivityType.COMPETING120
}121
]122
},123
streamLink: {124
type: OptionType.STRING,125
description: "Twitch.tv or Youtube.com link (only for Streaming activity type)",126
onChange: onChange,127
disabled: isStreamLinkDisabled,128
isValid: isStreamLinkValid129
},130
timestampMode: {131
type: OptionType.SELECT,132
description: "Timestamp mode",133
onChange: onChange,134
options: [135
{136
label: "None",137
value: TimestampMode.NONE,138
default: true139
},140
{141
label: "Since discord open",142
value: TimestampMode.NOW143
},144
{145
label: "Same as your current time",146
value: TimestampMode.TIME147
},148
{149
label: "Custom",150
value: TimestampMode.CUSTOM151
}152
]153
},154
startTime: {155
type: OptionType.NUMBER,156
description: "Start timestamp (only for custom timestamp mode)",157
onChange: onChange,158
disabled: isTimestampDisabled,159
isValid: (value: number) => {160
if (value && value < 0) return "Start timestamp must be greater than 0.";161
return true;162
}163
},164
endTime: {165
type: OptionType.NUMBER,166
description: "End timestamp (only for custom timestamp mode)",167
onChange: onChange,168
disabled: isTimestampDisabled,169
isValid: (value: number) => {170
if (value && value < 0) return "End timestamp must be greater than 0.";171
return true;172
}173
}174
});175
176
function onChange() {177
setRpc(true);178
setRpc();179
}180
181
function isStreamLinkDisabled() {182
return settings.store.type !== ActivityType.STREAMING;183
}184
185
function isStreamLinkValid(value: string) {186
if (!isStreamLinkDisabled() && !/https?:\/\/(www\.)?(twitch\.tv|youtube\.com)\/\w+/.test(value)) return "Streaming link must be a valid URL.";187
return true;188
}189
190
function isTimestampDisabled() {191
return settings.store.timestampMode !== TimestampMode.CUSTOM;192
}193
194
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////195
196
function onlineFriendCount(): number {197
let onlineFriends = 0;198
const relationships = RelationshipStore.getRelationships();199
for (const id in relationships) {200
if (relationships[id] === 1 && PresenceStore.getStatus(id) !== "offline") onlineFriends++;201
}202
return onlineFriends;203
}204
205
function totalFriendCount(): number {206
return Object.values(RelationshipStore.getRelationships()).filter(r => r === 1).length;207
}208
209
function memberCount(): string {210
const channelId = SelectedChannelStore.getChannelId();211
const guildId = SelectedGuildStore.getGuildId();212
const { groups } = ChannelMemberStore.getProps(guildId, channelId);213
const total = GuildMemberCountStore.getMemberCount(guildId);214
215
if (total == null)216
return "";217
218
const online =219
(groups.length === 1 && groups[0].id === "unknown")220
? 0221
: groups.reduce((count, curr) => count + (curr.id === "offline" ? 0 : curr.count), 0);222
223
return online === 0 ? `${total} members` : `${online} online / ${total} total`;224
}225
226
function getChannelIconURL(channel: Channel): string {227
if (channel.icon) return `https:class="ts-cmt">//cdn.discordapp.com/channel-icons/${channel.id}/${channel.icon}.webp?size=128`;228
return chino;229
}230
231
async function createActivity(): Promise<Activity | undefined> {232
233
const {234
appID,235
userAvatarAsSmallImage,236
exposeDmsUsername,237
streamLink,238
timestampMode,239
startTime,240
endTime,241
} = settings.store;242
243
let { type } = settings.store;244
245
let appName = "FiveCord";246
let details = "";247
let state = "";248
let imageBig = "";249
const imageBigTooltip = undefined;250
let imageSmall = "";251
const imageSmallTooltip = undefined;252
let buttonOneText: string | undefined;253
let buttonOneURL: string | undefined;254
let buttonTwoText: string | undefined;255
let buttonTwoURL: string | undefined;256
257
258
const channelId = SelectedChannelStore.getChannelId();259
const guildId = SelectedGuildStore.getGuildId();260
const voiceId = SelectedChannelStore.getVoiceChannelId();261
const currentUser = UserStore.getCurrentUser();262
if (userAvatarAsSmallImage) imageSmall = currentUser.getAvatarURL(undefined, undefined, true) || chino;263
264
if (!channelId) {265
appName = "Friends List";266
details = `${onlineFriendCount()} online / ${totalFriendCount()} total`;267
state = `${GuildStore.getGuildCount()} servers`;268
imageBig = chino;269
} else {270
if (channelId === "@home" || channelId === "customize-community" || channelId === "channel-browser" || channelId === "onboarding") {271
appName = channelId === "@home" ? "Server Guide" : channelId === "customize-community" ? "Channels & Roles" : channelId === "channel-browser" ? "Browse Channels" : "Onboarding";272
const guild = GuildStore.getGuild(guildId);273
if (guild) {274
details = guild.name;275
state = memberCount();276
imageBig = guild.getIconURL(128, true) || chino;277
if (guild.vanityURLCode) {278
buttonOneText = `Join ${guild.name.slice(0, 26)}`;279
buttonOneURL = `https:class="ts-cmt">//discord.gg/${guild.vanityURLCode}`;280
}281
}282
} else {283
284
const channel = ChannelStore.getChannel(channelId);285
286
if (channel.isDM()) {287
const recipient = UserStore.getUser(channel.recipients[0]);288
appName = exposeDmsUsername ? `${recipient.username}039;s DM` : "Direct Messages";289
details = `${onlineFriendCount()} online / ${totalFriendCount()} total`;290
state = `${GuildStore.getGuildCount()} servers`;291
imageBig = recipient.getAvatarURL(undefined, undefined, true) || chino;292
}293
294
if (channel.isGroupDM()) {295
appName = channel.name || "Group DM";296
details = `${channel.recipients.length + 1} members`;297
imageBig = getChannelIconURL(channel);298
}299
300
const guild = GuildStore.getGuild(guildId);301
if (guild) {302
appName = `#${channel.name}`;303
details = guild.name;304
state = memberCount();305
imageBig = guild.getIconURL(128, true) || chino;306
if (guild.vanityURLCode) {307
buttonOneText = `Join ${guild.name.slice(0, 31 - 5)}`;308
buttonOneURL = `https:class="ts-cmt">//discord.gg/${guild.vanityURLCode}`;309
}310
}311
}312
}313
314
if (voiceId) {315
const voiceChannel = ChannelStore.getChannel(voiceId);316
const voiceGuild = GuildStore.getGuild(voiceChannel.guild_id);317
const voiceMemberCount = Object.keys(VoiceStates.getVoiceStatesForChannel(voiceChannel.id)).length;318
buttonTwoText = `๐ ${voiceGuild.name.slice(0, 31 - 6 - voiceMemberCount.toString().length)} [${voiceMemberCount}]`;319
buttonTwoURL = `https:class="ts-cmt">//discordapp.com/channels/${voiceGuild.id}/${voiceChannel.id}`;320
321
if (!buttonOneText) {322
buttonOneText = buttonTwoText;323
buttonOneURL = buttonTwoURL;324
buttonTwoText = undefined;325
buttonTwoURL = undefined;326
}327
}328
329
if ((new Date().getHours() === 7 || new Date().getHours() === 19) && new Date().getMinutes() === 27) {330
type = ActivityType.PLAYING;331
appName = "WHEN YOU SEE IT";332
imageBig = wysi;333
details = "7:27 ๐";334
state = "";335
}336
337
// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////338
339
const activity: Activity = {340
application_id: appID || "0",341
name: appName,342
state,343
details,344
type,345
flags: 1 << 0,346
};347
348
if (type === ActivityType.STREAMING) activity.url = streamLink;349
350
switch (timestampMode) {351
case TimestampMode.NOW:352
activity.timestamps = {353
start: Date.now()354
};355
break;356
case TimestampMode.TIME:357
activity.timestamps = {358
start: Date.now() - (new Date().getHours() * 3600 + new Date().getMinutes() * 60 + new Date().getSeconds()) * 1000359
};360
break;361
case TimestampMode.CUSTOM:362
if (startTime || endTime) {363
activity.timestamps = {};364
if (startTime) activity.timestamps.start = startTime;365
if (endTime) activity.timestamps.end = endTime;366
}367
break;368
case TimestampMode.NONE:369
default:370
break;371
}372
373
if (buttonOneText) {374
activity.buttons = [375
buttonOneText,376
buttonTwoText377
].filter(isTruthy);378
379
activity.metadata = {380
button_urls: [381
buttonOneURL,382
buttonTwoURL383
].filter(isTruthy)384
};385
}386
387
if (imageBig) {388
activity.assets = {389
large_image: await getApplicationAsset(imageBig),390
large_text: imageBigTooltip || undefined391
};392
}393
394
if (imageSmall) {395
activity.assets = {396
...activity.assets,397
small_image: await getApplicationAsset(imageSmall),398
small_text: imageSmallTooltip || undefined399
};400
}401
402
403
for (const k in activity) {404
if (k === "type") continue;405
const v = activity[k];406
if (!v || v.length === 0)407
delete activity[k];408
}409
410
return activity;411
}412
413
let timeout: NodeJS.Timeout | null = null;414
415
async function setRpc(disable?: boolean) {416
const activities: any = presenceStore.getActivities();417
const activity: Activity | undefined = !activities.length || (activities.length === 1 && activities[0].application_id === settings.store.appID) ? await createActivity() : undefined;418
419
FluxDispatcher.dispatch({420
type: "LOCAL_ACTIVITY_UPDATE",421
activity: !disable ? activity : null,422
socketId: "CustomRPC",423
});424
425
if (!disable) {426
timeout = setTimeout(() => setRpc(), 4000);427
} else if (timeout) {428
clearTimeout(timeout);429
timeout = null;430
}431
}432
433
export default definePlugin({434
name: "FiveCordRPC",435
description: "how to expose yourself",436
authors: [Devs.FiveCord],437
start: setRpc,438
stop: () => setRpc(true),439
settings440
});441