From dcf322b83244664e5ff52c98198e4e4430ae8bea Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Sat, 28 Jun 2025 16:04:15 -0400 Subject: [PATCH] Added Orbolay Bridge --- src/equicordplugins/followVoiceUser/index.tsx | 3 +- src/equicordplugins/orbolayBridge/index.tsx | 314 ++++++++++++++++++ src/equicordplugins/randomVoice/index.tsx | 3 +- .../vcNarratorCustom/index.tsx | 7 +- src/equicordplugins/voiceChatUtils/index.tsx | 5 +- .../voiceJoinMessages/index.ts | 7 +- src/plugins/userVoiceShow/components.tsx | 5 +- src/plugins/vcNarrator/index.tsx | 5 +- src/webpack/common/stores.ts | 2 + 9 files changed, 326 insertions(+), 25 deletions(-) create mode 100644 src/equicordplugins/orbolayBridge/index.tsx diff --git a/src/equicordplugins/followVoiceUser/index.tsx b/src/equicordplugins/followVoiceUser/index.tsx index 49c6f52a..0fc0c393 100644 --- a/src/equicordplugins/followVoiceUser/index.tsx +++ b/src/equicordplugins/followVoiceUser/index.tsx @@ -11,7 +11,7 @@ import { definePluginSettings } from "@api/Settings"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy, findStoreLazy } from "@webpack"; -import { Forms, Menu, React } from "@webpack/common"; +import { Forms, Menu, React, VoiceStateStore } from "@webpack/common"; import { VoiceState } from "@webpack/types"; import { Channel, User } from "discord-types/general"; @@ -29,7 +29,6 @@ interface UserContextProps { let followedUserInfo: TFollowedUserInfo = null; const voiceChannelAction = findByPropsLazy("selectVoiceChannel"); -const VoiceStateStore = findStoreLazy("VoiceStateStore"); const UserStore = findStoreLazy("UserStore"); const RelationshipStore = findStoreLazy("RelationshipStore"); diff --git a/src/equicordplugins/orbolayBridge/index.tsx b/src/equicordplugins/orbolayBridge/index.tsx new file mode 100644 index 00000000..e4165fa7 --- /dev/null +++ b/src/equicordplugins/orbolayBridge/index.tsx @@ -0,0 +1,314 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 OpenAsar + * + * 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 . +*/ + +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import definePlugin, { OptionType } from "@utils/types"; +import { ChannelStore, FluxDispatcher, GuildMemberStore, Toasts, UserStore, VoiceStateStore } from "@webpack/common"; + +interface ChannelState { + userId: string; + channelId: string; + deaf: boolean; + mute: boolean; + stream: boolean; + selfDeaf: boolean; + selfMute: boolean; + selfStream: boolean; +} + +const settings = definePluginSettings({ + port: { + type: OptionType.NUMBER, + description: "Port to connect to", + default: 6888, + restartNeeded: true + }, + messageAlignment: { + type: OptionType.SELECT, + description: "Alignment of messages in the overlay", + options: [ + { label: "Top left", value: "topleft", default: true }, + { label: "Top right", value: "topright" }, + { label: "Bottom left", value: "bottomleft" }, + { label: "Bottom right", value: "bottomright" }, + ], + default: "topright", + restartNeeded: true + }, + userAlignment: { + type: OptionType.SELECT, + description: "Alignment of users in the overlay", + options: [ + { label: "Top left", value: "topleft", default: true }, + { label: "Top right", value: "topright" }, + { label: "Bottom left", value: "bottomleft" }, + { label: "Bottom right", value: "bottomright" }, + ], + default: "topleft", + restartNeeded: true + }, + voiceSemitransparent: { + type: OptionType.BOOLEAN, + description: "Make voice channel members transparent", + default: true, + restartNeeded: true + }, + messagesSemitransparent: { + type: OptionType.BOOLEAN, + description: "Make message notifications transparent", + default: false, + restartNeeded: true + }, +}); + +let ws: WebSocket | null = null; +let currentChannel = null; + +const waitForPopulate = async fn => { + while (true) { + const result = await fn(); + if (result) return result; + await new Promise(r => setTimeout(r, 500)); + } +}; + +function stateToPayload(guildId: string, state: ChannelState) { + const user = UserStore.getUser(state.userId); + const nickname = GuildMemberStore.getNick(guildId, state.userId); + return { + userId: state.userId, + username: nickname || (user as any).globalName || user.username, + avatarUrl: user.avatar, + channelId: state.channelId, + deaf: state.deaf || state.selfDeaf, + mute: state.mute || state.selfMute, + streaming: state.selfStream, + speaking: false, + }; +} + +const incoming = payload => { + switch (payload.cmd) { + case "TOGGLE_MUTE": + FluxDispatcher.dispatch({ + type: "AUDIO_TOGGLE_SELF_MUTE", + syncRemote: true, + playSoundEffect: true, + context: "default" + }); + break; + case "TOGGLE_DEAF": + FluxDispatcher.dispatch({ + type: "AUDIO_TOGGLE_SELF_DEAF", + syncRemote: true, + playSoundEffect: true, + context: "default" + }); + break; + case "DISCONNECT": + FluxDispatcher.dispatch({ + type: "VOICE_CHANNEL_SELECT", + channelId: null + }); + break; + case "STOP_STREAM": { + const userId = UserStore.getCurrentUser()?.id; + const voiceState = VoiceStateStore.getVoiceStateForUser(userId); + const channel = ChannelStore.getChannel(voiceState.channelId); + + // If any of these are null, we can't do anything + if (!userId || !voiceState || !channel) return; + + FluxDispatcher.dispatch({ + type: "STREAM_STOP", + streamKey: `guild:${channel.guild_id}:${voiceState.channelId}:${userId}`, + appContext: "APP" + }); + } + } +}; + +const handleSpeaking = dispatch => { + ws?.send( + JSON.stringify({ + cmd: "VOICE_STATE_UPDATE", + state: { + userId: dispatch.userId, + speaking: dispatch.speakingFlags === 1, + }, + }) + ); +}; + +const handleMessageNotification = dispatch => { + ws?.send( + JSON.stringify({ + cmd: "MESSAGE_NOTIFICATION", + message: { + title: dispatch.title, + body: dispatch.body, + icon: dispatch.icon, + channelId: dispatch.channelId, + } + }) + ); +}; + +const handleVoiceStateUpdates = async dispatch => { + // Ensure we are in the channel that the update is for + const { id } = UserStore.getCurrentUser(); + + for (const state of dispatch.voiceStates) { + const ourState = state.userId === id; + const { guildId } = state; + + if (ourState) { + if (state.channelId && state.channelId !== currentChannel) { + const voiceStates = await waitForPopulate(() => + VoiceStateStore?.getVoiceStatesForChannel(state.channelId) + ); + + ws?.send( + JSON.stringify({ + cmd: "CHANNEL_JOINED", + states: Object.values(voiceStates).map(s => stateToPayload(guildId, s as ChannelState)), + }) + ); + + currentChannel = state.channelId; + + break; + } else if (!state.channelId) { + ws?.send( + JSON.stringify({ + cmd: "CHANNEL_LEFT", + }) + ); + + currentChannel = null; + + break; + } + } + + // If this is for the channel we are in, send a VOICE_STATE_UPDATE + if ( + !!currentChannel && + (state.channelId === currentChannel || + state.oldChannelId === currentChannel) + ) { + ws?.send( + JSON.stringify({ + cmd: "VOICE_STATE_UPDATE", + state: stateToPayload(guildId, state as ChannelState), + }) + ); + } + } +}; + +const createWebsocket = () => { + console.log("Attempting to connect to Orbolay server"); + + // First ensure old connection is closed + if (ws?.close) ws.close(); + + setTimeout(() => { + // If the ws is not ready, kill it and log + if (ws?.readyState !== WebSocket.OPEN) { + Toasts.show({ + message: "Orbolay websocket could not connect. Is it running?", + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + }); + ws = null; + return; + } + }, 1000); + + ws = new WebSocket("ws://127.0.0.1:" + settings.store.port); + ws.onerror = e => { + ws?.close?.(); + ws = null; + throw e; + }; + ws.onmessage = e => { + incoming(JSON.parse(e.data)); + }; + ws.onclose = () => { + ws = null; + }; + ws.onopen = async () => { + Toasts.show({ + message: "Connected to Orbolay server", + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + }); + + // Send over the config + const config = { + ...settings.store, + userId: null, + }; + + // Ensure we track the current user id + config.userId = await waitForPopulate(() => UserStore.getCurrentUser().id); + + ws?.send(JSON.stringify({ cmd: "REGISTER_CONFIG", ...config })); + + // Send initial channel joined (if the user is in a channel) + const userVoiceState = VoiceStateStore.getVoiceStateForUser(config.userId); + + if (!userVoiceState) return; + + const channelState = VoiceStateStore.getVoiceStatesForChannel(userVoiceState.channelId); + const { guildId } = userVoiceState; + + ws?.send( + JSON.stringify({ + cmd: "CHANNEL_JOINED", + states: Object.values(channelState).map(s => stateToPayload(guildId, s as ChannelState)), + }) + ); + + currentChannel = userVoiceState.channelId; + }; +}; + +export default definePlugin({ + name: "Orbolay Bridge", + description: "Bridge plugin to connect Orbolay to Discord", + authors: [EquicordDevs.SpikeHD], + settings, + start() { + createWebsocket(); + + FluxDispatcher.subscribe("SPEAKING", handleSpeaking); + FluxDispatcher.subscribe("VOICE_STATE_UPDATES", handleVoiceStateUpdates); + FluxDispatcher.subscribe("RPC_NOTIFICATION_CREATE", handleMessageNotification); + }, + stop() { + ws?.close(); + ws = null; + + FluxDispatcher.unsubscribe("SPEAKING", handleSpeaking); + FluxDispatcher.unsubscribe("VOICE_STATE_UPDATES", handleVoiceStateUpdates); + FluxDispatcher.unsubscribe("RPC_NOTIFICATION_CREATE", handleMessageNotification); + } +}); diff --git a/src/equicordplugins/randomVoice/index.tsx b/src/equicordplugins/randomVoice/index.tsx index 96ea92f9..c7d5cb87 100644 --- a/src/equicordplugins/randomVoice/index.tsx +++ b/src/equicordplugins/randomVoice/index.tsx @@ -12,13 +12,12 @@ import { debounce } from "@shared/debounce"; import { EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByCode, findByProps, findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack"; -import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore } from "@webpack/common"; +import { ChannelRouter, ChannelStore, ContextMenuApi, GuildStore, Menu, PermissionsBits, PermissionStore, React, SelectedChannelStore, Toasts, UserStore, VoiceStateStore } from "@webpack/common"; import style from "./styles.css?managed"; const ChatVoiceIcon = findComponentByCodeLazy("0l1.8-1.8c.17"); const Button = findComponentByCodeLazy(".NONE,disabled:", ".PANEL_BUTTON"); -const VoiceStateStore = findStoreLazy("VoiceStateStore"); const MediaEngineStore = findStoreLazy("MediaEngineStore"); const ChannelActions = findByPropsLazy("selectChannel", "selectVoiceChannel"); const { toggleSelfMute } = findByPropsLazy("toggleSelfMute"); diff --git a/src/equicordplugins/vcNarratorCustom/index.tsx b/src/equicordplugins/vcNarratorCustom/index.tsx index 471974ca..fa8d8b92 100644 --- a/src/equicordplugins/vcNarratorCustom/index.tsx +++ b/src/equicordplugins/vcNarratorCustom/index.tsx @@ -11,7 +11,6 @@ import { wordsToTitle } from "@utils/text"; import definePlugin, { OptionType, } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; import { Button, ChannelStore, @@ -22,6 +21,7 @@ import { SelectedGuildStore, useMemo, UserStore, + VoiceStateStore, } from "@webpack/common"; // Create an in-memory cache (temporary, lost on restart) @@ -77,11 +77,6 @@ interface VoiceState { selfMute: boolean; } -const VoiceStateStore = findByPropsLazy( - "getVoiceStatesForChannel", - "getCurrentClientVoiceChannelId" -); - // Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying // Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would // not say the second mute, which would lead you to believe they're unmuted diff --git a/src/equicordplugins/voiceChatUtils/index.tsx b/src/equicordplugins/voiceChatUtils/index.tsx index ec232cf6..3c3b3998 100644 --- a/src/equicordplugins/voiceChatUtils/index.tsx +++ b/src/equicordplugins/voiceChatUtils/index.tsx @@ -9,12 +9,9 @@ import { definePluginSettings } from "@api/Settings"; import { makeRange } from "@components/PluginSettings/components"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { findStoreLazy } from "@webpack"; -import { GuildChannelStore, Menu, React, RestAPI, UserStore } from "@webpack/common"; +import { GuildChannelStore, Menu, React, RestAPI, UserStore, VoiceStateStore } from "@webpack/common"; import type { Channel } from "discord-types/general"; -const VoiceStateStore = findStoreLazy("VoiceStateStore"); - async function runSequential(promises: Promise[]): Promise { const results: T[] = []; diff --git a/src/equicordplugins/voiceJoinMessages/index.ts b/src/equicordplugins/voiceJoinMessages/index.ts index ea2048d4..f4adf6b9 100644 --- a/src/equicordplugins/voiceJoinMessages/index.ts +++ b/src/equicordplugins/voiceJoinMessages/index.ts @@ -8,12 +8,11 @@ import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import { humanFriendlyJoin } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; -import { findByCodeLazy, findByPropsLazy } from "@webpack"; -import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore } from "@webpack/common"; +import { findByCodeLazy } from "@webpack"; +import { ChannelStore, FluxDispatcher, MessageActions, MessageStore, RelationshipStore, SelectedChannelStore, UserStore, VoiceStateStore } from "@webpack/common"; import { Message, User } from "discord-types/general"; const createBotMessage = findByCodeLazy('username:"Clyde"'); -const SortedVoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); const settings = definePluginSettings({ friendDirectMessages: { @@ -127,7 +126,7 @@ export default definePlugin({ const selfInChannel = SelectedChannelStore.getVoiceChannelId() === channelId; let memberListContent = ""; if (settings.store.friendDirectMessagesShowMembers || settings.store.friendDirectMessagesShowMemberCount) { - const voiceState = SortedVoiceStateStore.getVoiceStatesForChannel(channelId); + const voiceState = VoiceStateStore.getVoiceStatesForChannel(channelId); const sortedVoiceStates: User[] = Object.values(voiceState as { [key: string]: VoiceState; }) .filter((voiceState: VoiceState) => { voiceState.user && voiceState.user.id !== userId; }) .map((voiceState: VoiceState) => voiceState.user); diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx index 136febd6..6f3780ef 100644 --- a/src/plugins/userVoiceShow/components.tsx +++ b/src/plugins/userVoiceShow/components.tsx @@ -7,8 +7,8 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { classes } from "@utils/misc"; -import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy, mapMangledModuleLazy } from "@webpack"; -import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, useStateFromStores } from "@webpack/common"; +import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack"; +import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, useStateFromStores, VoiceStateStore } from "@webpack/common"; import { Channel } from "discord-types/general"; const cl = classNameFactory("vc-uvs-"); @@ -18,7 +18,6 @@ const { useChannelName } = mapMangledModuleLazy("#{intl::GROUP_DM_ALONE}", { useChannelName: filters.byCode("()=>null==") }); const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({"); -const VoiceStateStore = findStoreLazy("VoiceStateStore"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const Avatar = findComponentByCodeLazy(".status)/2):0"); diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx index bd4685e2..bd75c112 100644 --- a/src/plugins/vcNarrator/index.tsx +++ b/src/plugins/vcNarrator/index.tsx @@ -22,8 +22,7 @@ import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { wordsToTitle } from "@utils/text"; import definePlugin, { ReporterTestable } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common"; +import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore, VoiceStateStore } from "@webpack/common"; import { ReactElement } from "react"; import { getCurrentVoice, settings } from "./settings"; @@ -38,8 +37,6 @@ interface VoiceState { selfMute: boolean; } -const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); - // Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying // Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would // not say the second mute, which would lead you to believe they're unmuted diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 0d24ad93..d00649c7 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -38,6 +38,7 @@ export let PermissionStore: GenericStore; export let GuildChannelStore: GenericStore; export let ReadStateStore: GenericStore; export let PresenceStore: GenericStore; +export let VoiceStateStore: GenericStore; export let GuildStore: t.GuildStore; export let UserStore: Stores.UserStore & t.FluxStore; @@ -90,3 +91,4 @@ waitForStore("ThemeStore", m => { // Importing this directly can easily cause circular imports. For this reason, use a non import access here. Vencord.QuickCss.initQuickCssThemeStore(); }); +waitForStore("VoiceStateStore", m => VoiceStateStore = m);