diff --git a/src/actions/channel.rs b/src/actions/channel.rs index a9e6cd1..c4f3079 100644 --- a/src/actions/channel.rs +++ b/src/actions/channel.rs @@ -1,8 +1,8 @@ -use crate::cache::{CachedGuild, guild_cache, refresh_guild_cache}; -use crate::client::{current_voice_channel, discord_client}; +use crate::cache::{CachedGuild, GUILD_CACHE, refresh_guild_cache}; +use crate::client::{CURRENT_VOICE_CHANNEL, get_discord_client}; use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc, LazyLock}; use discord_ipc_rust::models::send::commands::{ GetChannelsArgs, SelectTextChannelArgs, SelectVoiceChannelArgs, SentCommand, @@ -12,7 +12,7 @@ use openaction::{ Action, ActionUuid, Instance, InstanceId, OpenActionResult, async_trait, visible_instances, }; use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock; +use tokio::sync::Mutex; #[derive(Clone, Copy)] enum ChannelKind { @@ -41,10 +41,8 @@ impl ChannelKind { } } -fn channel_request_map() -> &'static RwLock> { - static REQUESTS: OnceLock>> = OnceLock::new(); - REQUESTS.get_or_init(|| RwLock::new(HashMap::new())) -} +static CHANNEL_REQUESTS_MAP: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); async fn get_all_instances() -> impl Iterator> { visible_instances(TextChannelAction::UUID) @@ -60,7 +58,7 @@ pub async fn send_guilds_to_pi(instance: Option<&Instance>) { guilds: Vec, } - let cache = guild_cache().read().await; + let cache = GUILD_CACHE.read().await; let payload = Payload { guilds: cache.clone(), }; @@ -78,7 +76,7 @@ pub async fn send_guilds_to_pi(instance: Option<&Instance>) { } pub async fn send_cached_guilds_to_pi(instance: &Instance) -> OpenActionResult<()> { - if !guild_cache().read().await.is_empty() { + if !GUILD_CACHE.read().await.is_empty() { send_guilds_to_pi(Some(instance)).await; Ok(()) } else { @@ -97,7 +95,7 @@ pub async fn send_channels_to_pi(channels: &[Channel]) { channels: Vec, } - let mut requests = channel_request_map().write().await; + let mut requests = CHANNEL_REQUESTS_MAP.lock().await; for instance in get_all_instances().await { if let Some(kind) = requests.remove(&instance.instance_id) { @@ -136,17 +134,22 @@ impl PiRequest { match request { PiRequest::RequestChannels { guild_id } => { - channel_request_map() - .write() + CHANNEL_REQUESTS_MAP + .lock() .await .insert(instance.instance_id.clone(), kind); - let mut client_lock = discord_client().write().await; - if let Some(client) = client_lock.as_mut() - && let Err(e) = client + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client .emit_command(&SentCommand::GetChannels(GetChannelsArgs { guild_id })) .await - { + }; + + if let Err(e) = result { log::error!("Failed to request channels: {}", e); } } @@ -191,20 +194,20 @@ impl Action for TextChannelAction { return Ok(()); } - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::SelectTextChannel(SelectTextChannelArgs { + channel_id: Some(settings.channel_id.clone()), + timeout: None, + })) + .await }; - if let Err(e) = client - .emit_command(&SentCommand::SelectTextChannel(SelectTextChannelArgs { - channel_id: Some(settings.channel_id.clone()), - timeout: None, - })) - .await - { + if let Err(e) = result { log::error!("Failed to select text channel: {}", e); instance.show_alert().await?; } @@ -217,7 +220,7 @@ async fn sync_voice_channel_state( instance: &Instance, settings: &ChannelActionSettings, ) -> OpenActionResult<()> { - let is_active = current_voice_channel() + let is_active = CURRENT_VOICE_CHANNEL .read() .await .as_deref() @@ -271,14 +274,7 @@ impl Action for VoiceChannelAction { return Ok(()); } - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); - }; - - let current = current_voice_channel().read().await; + let current = CURRENT_VOICE_CHANNEL.read().await; let target = if current.as_deref() != Some(settings.channel_id.as_str()) { Some(settings.channel_id.clone()) } else { @@ -286,15 +282,22 @@ impl Action for VoiceChannelAction { }; drop(current); - if let Err(e) = client - .emit_command(&SentCommand::SelectVoiceChannel(SelectVoiceChannelArgs { - channel_id: target, - force: Some(true), - navigate: Some(false), - timeout: None, - })) - .await - { + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::SelectVoiceChannel(SelectVoiceChannelArgs { + channel_id: target, + force: Some(true), + navigate: Some(false), + timeout: None, + })) + .await + }; + + if let Err(e) = result { log::error!("Failed to select or deselect voice channel: {}", e); instance.show_alert().await?; } diff --git a/src/actions/notifications.rs b/src/actions/notifications.rs index cab4172..423fb00 100644 --- a/src/actions/notifications.rs +++ b/src/actions/notifications.rs @@ -1,12 +1,12 @@ -use crate::cache::notification_cache; -use crate::client::discord_client; +use crate::cache::NOTIFICATION_CACHE; +use crate::client::get_discord_client; use discord_ipc_rust::models::send::commands::{SelectTextChannelArgs, SentCommand}; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait}; use serde::{Deserialize, Serialize}; pub async fn update_title(instance: &Instance) -> OpenActionResult<()> { - let cache = notification_cache().read().await; + let cache = NOTIFICATION_CACHE.read().await; let title = format!("{}", cache.len()); if let Err(e) = instance.set_title(Some(title), None).await { @@ -55,21 +55,21 @@ impl Action for NotificationsAction { let notification = match settings.action_type { NotificationsActionType::DoNothing => return Ok(()), NotificationsActionType::Clear => { - notification_cache().write().await.clear(); + NOTIFICATION_CACHE.write().await.clear(); update_title(instance).await?; return Ok(()); } NotificationsActionType::OpenAndClear => { - let mut cache = notification_cache().write().await; + let mut cache = NOTIFICATION_CACHE.write().await; let notification = cache.pop_back(); cache.clear(); notification } NotificationsActionType::CycleRecentFirst => { - notification_cache().write().await.pop_back() + NOTIFICATION_CACHE.write().await.pop_back() } NotificationsActionType::CycleOldestFirst => { - notification_cache().write().await.pop_front() + NOTIFICATION_CACHE.write().await.pop_front() } }; @@ -80,20 +80,20 @@ impl Action for NotificationsAction { update_title(instance).await?; - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::SelectTextChannel(SelectTextChannelArgs { + channel_id: Some(notification.channel_id), + timeout: None, + })) + .await }; - if let Err(e) = client - .emit_command(&SentCommand::SelectTextChannel(SelectTextChannelArgs { - channel_id: Some(notification.channel_id), - timeout: None, - })) - .await - { + if let Err(e) = result { log::error!("Failed to select text channel: {}", e); instance.show_alert().await?; } diff --git a/src/actions/screen_share.rs b/src/actions/screen_share.rs index f708287..33fac5e 100644 --- a/src/actions/screen_share.rs +++ b/src/actions/screen_share.rs @@ -1,4 +1,4 @@ -use crate::client::discord_client; +use crate::client::get_discord_client; use std::collections::HashMap; @@ -17,19 +17,19 @@ impl Action for ToggleScreenshareAction { instance: &Instance, _settings: &Self::Settings, ) -> OpenActionResult<()> { - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::ToggleScreenshare(ToggleScreenshareArgs { + pid: None, + })) + .await }; - if let Err(e) = client - .emit_command(&SentCommand::ToggleScreenshare(ToggleScreenshareArgs { - pid: None, - })) - .await - { + if let Err(e) = result { log::error!("Failed to toggle screen share: {}", e); instance.show_alert().await?; } diff --git a/src/actions/soundboard.rs b/src/actions/soundboard.rs index e215f7f..81e1195 100644 --- a/src/actions/soundboard.rs +++ b/src/actions/soundboard.rs @@ -1,5 +1,5 @@ -use crate::cache::{CachedSoundboardSound, refresh_soundboard_cache, soundboard_sounds_cache}; -use crate::client::discord_client; +use crate::cache::{CachedSoundboardSound, SOUNDBOARD_SOUNDS_CACHE, refresh_soundboard_cache}; +use crate::client::get_discord_client; use discord_ipc_rust::models::send::commands::SentCommand; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait, visible_instances}; @@ -12,7 +12,7 @@ pub async fn send_sounds_to_pi(instance: Option<&Instance>) { } let payload = Payload { - sounds: soundboard_sounds_cache().read().await.clone(), + sounds: SOUNDBOARD_SOUNDS_CACHE.read().await.clone(), }; match instance { @@ -33,7 +33,7 @@ async fn set_button_title(instance: &Instance, sound: Option<&CachedSoundboardSo } async fn send_cached_sounds_to_pi(instance: &Instance) -> OpenActionResult<()> { - if !soundboard_sounds_cache().read().await.is_empty() { + if !SOUNDBOARD_SOUNDS_CACHE.read().await.is_empty() { send_sounds_to_pi(Some(instance)).await; crate::actions::channel::send_cached_guilds_to_pi(instance).await?; Ok(()) @@ -86,17 +86,17 @@ impl Action for SoundboardAction { return Ok(()); }; - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::PlaySoundboardSound(sound.clone().into())) + .await }; - if let Err(e) = client - .emit_command(&SentCommand::PlaySoundboardSound(sound.clone().into())) - .await - { + if let Err(e) = result { log::error!("Failed to play soundboard sound: {}", e); instance.show_alert().await?; } diff --git a/src/actions/video.rs b/src/actions/video.rs index 5dc1ad8..7e03ebc 100644 --- a/src/actions/video.rs +++ b/src/actions/video.rs @@ -1,4 +1,4 @@ -use crate::client::discord_client; +use crate::client::get_discord_client; use std::collections::HashMap; @@ -17,14 +17,15 @@ impl Action for ToggleVideoAction { instance: &Instance, _settings: &Self::Settings, ) -> OpenActionResult<()> { - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client.emit_command(&SentCommand::ToggleVideo).await }; - if let Err(e) = client.emit_command(&SentCommand::ToggleVideo).await { + if let Err(e) = result { log::error!("Failed to toggle video: {}", e); instance.show_alert().await?; } diff --git a/src/actions/voice_settings.rs b/src/actions/voice_settings.rs index e9d31bd..bfd3b99 100644 --- a/src/actions/voice_settings.rs +++ b/src/actions/voice_settings.rs @@ -1,4 +1,3 @@ -pub mod audio_device_utils; pub mod set_audio_device; mod user_volume_control; mod volume_control; @@ -7,22 +6,14 @@ pub use set_audio_device::SetAudioDeviceAction; pub use user_volume_control::UserVolumeControlAction; pub use volume_control::VolumeControlAction; -use crate::client::discord_client; +use crate::client::{CURRENT_VOICE_MODE, get_discord_client}; use std::collections::HashMap; -use std::sync::OnceLock; use std::sync::atomic::Ordering::Relaxed; use discord_ipc_rust::models::send::commands::{SentCommand, SetVoiceSettingsArgs}; use discord_ipc_rust::models::shared::voice::VoiceSettingsMode; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait}; -use tokio::sync::RwLock; - -// Last-known voice mode from Discord, updated via RPC events. -pub fn current_voice_mode() -> &'static RwLock> { - static MODE: OnceLock>> = OnceLock::new(); - MODE.get_or_init(|| RwLock::new(None)) -} // Centralize the voice settings RPC call and Stream Deck feedback logic. async fn update_voice_setting( @@ -30,19 +21,18 @@ async fn update_voice_setting( args: SetVoiceSettingsArgs, next_state: usize, ) -> OpenActionResult<()> { - // Take the shared IPC client so we can send the voice update command. - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::SetVoiceSettings(args)) + .await }; // Send the RPC and update the Stream Deck feedback depending on the result. - match client - .emit_command(&SentCommand::SetVoiceSettings(args)) - .await - { + match result { Ok(_) => { // Reflect the new voice state on the button. instance.set_state(next_state as u16).await?; @@ -197,7 +187,7 @@ impl Action for ToggleVoiceInputModeAction { instance: &Instance, _settings: &Self::Settings, ) -> OpenActionResult<()> { - let mode_lock = current_voice_mode().read().await; + let mode_lock = CURRENT_VOICE_MODE.read().await; let Some(current_mode) = mode_lock.as_ref() else { log::error!("Voice mode not yet known"); instance.show_alert().await?; diff --git a/src/actions/voice_settings/set_audio_device.rs b/src/actions/voice_settings/set_audio_device.rs index 4aee9cc..9bef9b4 100644 --- a/src/actions/voice_settings/set_audio_device.rs +++ b/src/actions/voice_settings/set_audio_device.rs @@ -1,5 +1,8 @@ -use super::audio_device_utils::{AudioDeviceType, AudioDeviceWrapper, get_audio_device_settings}; use super::update_voice_setting; +use crate::{ + audio_device_utils::{AudioDeviceType, AudioDeviceWrapper}, + client::get_audio_device_settings, +}; use discord_ipc_rust::models::shared::voice::VoiceAvailableDevice; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait}; @@ -36,12 +39,7 @@ async fn update_device( device_type: &AudioDeviceType, device_id: String, ) -> OpenActionResult<()> { - let Some(current) = get_audio_device_settings(device_type).await else { - log::error!( - "Failed to obtain voice settings for {:?} device", - device_type - ); - instance.show_alert().await?; + let Some(current) = get_audio_device_settings(instance, device_type).await? else { return Ok(()); }; @@ -78,17 +76,19 @@ pub async fn send_available_devices_to_pi(instance: &Instance) -> OpenActionResu } async fn fetch_device_list( + instance: &Instance, device_type: &AudioDeviceType, - ) -> (String, Vec) { - get_audio_device_settings(device_type) - .await + ) -> OpenActionResult<(String, Vec)> { + Ok(get_audio_device_settings(instance, device_type) + .await? .map(|s| (s.device_id, s.available_devices)) - .unwrap_or_default() + .unwrap_or_default()) } - let (selected_input_device, input_devices) = fetch_device_list(&AudioDeviceType::Input).await; + let (selected_input_device, input_devices) = + fetch_device_list(instance, &AudioDeviceType::Input).await?; let (selected_output_device, output_devices) = - fetch_device_list(&AudioDeviceType::Output).await; + fetch_device_list(instance, &AudioDeviceType::Output).await?; instance .send_to_property_inspector(Payload { diff --git a/src/actions/voice_settings/user_volume_control.rs b/src/actions/voice_settings/user_volume_control.rs index e514832..789fc89 100644 --- a/src/actions/voice_settings/user_volume_control.rs +++ b/src/actions/voice_settings/user_volume_control.rs @@ -1,7 +1,5 @@ -use super::audio_device_utils::AudioDeviceType; - -use crate::actions::audio_device_utils::user_voice_settings_map; -use crate::client::discord_client; +use crate::audio_device_utils::AudioDeviceType; +use crate::client::{USER_VOICE_SETTINGS_MAP, get_discord_client}; use discord_ipc_rust::models::send::commands::{SentCommand, SetUserVoiceSettingsArgs}; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait}; @@ -40,17 +38,17 @@ async fn update_user_voice_settings( instance: &Instance, args: SetUserVoiceSettingsArgs, ) -> OpenActionResult<()> { - let mut client_lock = discord_client().write().await; - let Some(client) = client_lock.as_mut() else { - log::error!("Discord client not initialized"); - instance.show_alert().await?; - return Ok(()); + let reuslt = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client + .emit_command(&SentCommand::SetUserVoiceSettings(args)) + .await }; - if let Err(e) = client - .emit_command(&SentCommand::SetUserVoiceSettings(args)) - .await - { + if let Err(e) = reuslt { log::error!("Failed to update user voice settings: {}", e); instance.show_alert().await?; } @@ -66,7 +64,7 @@ async fn adjust_user_volume( ) -> OpenActionResult<()> { let device_type = AudioDeviceType::Output; - let current_volume = match user_voice_settings_map().read().await.get(&user_id) { + let current_volume = match USER_VOICE_SETTINGS_MAP.read().await.get(&user_id) { Some(settings) => settings.volume, None => { log::error!( @@ -112,7 +110,7 @@ async fn send_users_to_pi(instance: &Instance) -> OpenActionResult<()> { users: Vec, } - let users = user_voice_settings_map() + let users = USER_VOICE_SETTINGS_MAP .read() .await .iter() @@ -158,7 +156,7 @@ impl Action for UserVolumeControlAction { }; if matches!(settings.action_type, UserVolumeControlActionType::Mute) { - let new_mute_state = match user_voice_settings_map().read().await.get(user_id) { + let new_mute_state = match USER_VOICE_SETTINGS_MAP.read().await.get(user_id) { Some(settings) => !settings.mute, None => { log::error!( diff --git a/src/actions/voice_settings/volume_control.rs b/src/actions/voice_settings/volume_control.rs index 793c9a3..aca11be 100644 --- a/src/actions/voice_settings/volume_control.rs +++ b/src/actions/voice_settings/volume_control.rs @@ -1,5 +1,8 @@ -use super::audio_device_utils::{AudioDeviceType, AudioDeviceWrapper, get_audio_device_settings}; use super::update_voice_setting; +use crate::{ + audio_device_utils::{AudioDeviceType, AudioDeviceWrapper}, + client::get_audio_device_settings, +}; use openaction::{Action, ActionUuid, Instance, OpenActionResult, async_trait}; use serde::{Deserialize, Serialize}; @@ -38,12 +41,7 @@ async fn adjust_volume( value: f32, set: bool, ) -> OpenActionResult<()> { - let Some(device_settings) = get_audio_device_settings(device_type).await else { - log::error!( - "Failed to obtain voice settings for {:?} device", - device_type - ); - instance.show_alert().await?; + let Some(device_settings) = get_audio_device_settings(instance, device_type).await? else { return Ok(()); }; diff --git a/src/actions/voice_settings/audio_device_utils.rs b/src/audio_device_utils.rs similarity index 71% rename from src/actions/voice_settings/audio_device_utils.rs rename to src/audio_device_utils.rs index 3e805fd..abeca84 100644 --- a/src/actions/voice_settings/audio_device_utils.rs +++ b/src/audio_device_utils.rs @@ -1,12 +1,9 @@ -use std::{collections::HashMap, sync::OnceLock}; - use discord_ipc_rust::models::{ receive::events::VoiceStateData, send::commands::SetVoiceSettingsArgs, shared::voice::{VoiceAvailableDevice, VoiceSettingsInput, VoiceSettingsOutput}, }; use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum AudioDeviceType { @@ -105,30 +102,3 @@ impl From for UserVoiceSettings { } } } - -pub fn audio_input_settings() -> &'static RwLock> { - static SETTINGS: OnceLock>> = OnceLock::new(); - SETTINGS.get_or_init(|| RwLock::new(None)) -} - -pub fn audio_output_settings() -> &'static RwLock> { - static SETTINGS: OnceLock>> = OnceLock::new(); - SETTINGS.get_or_init(|| RwLock::new(None)) -} - -pub fn user_voice_settings_map() -> &'static RwLock> { - static MAP: OnceLock>> = OnceLock::new(); - MAP.get_or_init(Default::default) -} - -pub async fn get_audio_device_settings( - device_type: &AudioDeviceType, -) -> Option { - match device_type { - AudioDeviceType::Input => audio_input_settings(), - AudioDeviceType::Output => audio_output_settings(), - } - .read() - .await - .clone() -} diff --git a/src/cache.rs b/src/cache.rs index b76f826..cccc235 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,7 +1,7 @@ -use crate::client::discord_client; +use crate::client::get_discord_client; use std::collections::VecDeque; -use std::sync::OnceLock; +use std::sync::LazyLock; use discord_ipc_rust::models::{ receive::events::NotificationCreateData, @@ -36,25 +36,19 @@ impl From for PlaySoundboardSoundArgs { } } -pub fn guild_cache() -> &'static RwLock> { - static CACHE: OnceLock>> = OnceLock::new(); - CACHE.get_or_init(|| RwLock::new(Vec::new())) -} - -pub fn soundboard_sounds_cache() -> &'static RwLock> { - static CACHE: OnceLock>> = OnceLock::new(); - CACHE.get_or_init(|| RwLock::new(Vec::new())) -} - #[derive(Serialize, Clone)] pub struct CachedNotification { pub channel_id: String, } -pub fn notification_cache() -> &'static RwLock> { - static CACHE: OnceLock>> = OnceLock::new(); - CACHE.get_or_init(|| RwLock::new(VecDeque::new())) -} +pub static GUILD_CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +pub static SOUNDBOARD_SOUNDS_CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(Vec::new())); + +pub static NOTIFICATION_CACHE: LazyLock>> = + LazyLock::new(|| RwLock::new(VecDeque::new())); pub async fn update_guild_cache(guilds: &[Guild]) { let mut cached: Vec = guilds @@ -65,14 +59,19 @@ pub async fn update_guild_cache(guilds: &[Guild]) { }) .collect(); cached.sort_by_key(|x| x.name.to_lowercase()); - *guild_cache().write().await = cached; + *GUILD_CACHE.write().await = cached; } pub async fn refresh_guild_cache(instance: &Instance) -> OpenActionResult<()> { - let mut client_lock = discord_client().write().await; - if let Some(client) = client_lock.as_mut() - && let Err(e) = client.emit_command(&SentCommand::GetGuilds).await - { + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client.emit_command(&SentCommand::GetGuilds).await + }; + + if let Err(e) = result { log::error!("Failed to request guilds: {}", e); instance.show_alert().await?; } @@ -92,14 +91,19 @@ pub async fn update_soundboard_cache(sounds: &[SoundboardSound]) { }) .collect(); cached.sort_by_key(|x| x.name.to_lowercase()); - *soundboard_sounds_cache().write().await = cached; + *SOUNDBOARD_SOUNDS_CACHE.write().await = cached; } pub async fn refresh_soundboard_cache(instance: &Instance) -> OpenActionResult<()> { - let mut client_lock = discord_client().write().await; - if let Some(client) = client_lock.as_mut() - && let Err(e) = client.emit_command(&SentCommand::GetSoundboardSounds).await - { + let result = { + let Some(mut client) = get_discord_client(instance).await? else { + return Ok(()); + }; + + client.emit_command(&SentCommand::GetSoundboardSounds).await + }; + + if let Err(e) = result { log::error!("Failed to request soundboard sounds: {}", e); instance.show_alert().await?; } @@ -108,7 +112,7 @@ pub async fn refresh_soundboard_cache(instance: &Instance) -> OpenActionResult<( } pub async fn add_notification_to_cache(notification: NotificationCreateData) { - let mut cache_lock = notification_cache().write().await; + let mut cache_lock = NOTIFICATION_CACHE.write().await; cache_lock.push_back(CachedNotification { channel_id: notification.channel_id, }); diff --git a/src/client.rs b/src/client.rs index 4065ea3..d06ee7b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,11 @@ +use crate::audio_device_utils::{AudioDeviceType, AudioDeviceWrapper, UserVoiceSettings}; use crate::oauth::exchange_code_for_token; use crate::rpc_events::handle_rpc_event; -use crate::{DiscordSettings, current_settings}; +use crate::{CURRENT_SETTINGS, DiscordSettings}; +use std::collections::HashMap; use std::sync::{ - OnceLock, + LazyLock, atomic::{AtomicBool, Ordering}, }; @@ -11,31 +13,78 @@ use discord_ipc_rust::DiscordIpcClient; use discord_ipc_rust::models::receive::{ReceivedItem, commands::ReturnedCommand}; use discord_ipc_rust::models::send::commands::{AuthorizeArgs, SentCommand}; use discord_ipc_rust::models::send::events::SubscribeableEvent; -use openaction::set_global_settings; -use tokio::sync::RwLock; +use discord_ipc_rust::models::shared::voice::VoiceSettingsMode; +use openaction::{Instance, OpenActionResult, set_global_settings}; +use tokio::sync::{MappedMutexGuard, Mutex, MutexGuard, RwLock}; use tokio::time::{Duration, sleep}; // Shared place to store the active Discord IPC connection for the lifetime of the plugin. -pub fn discord_client() -> &'static RwLock> { - static CLIENT: OnceLock>> = OnceLock::new(); - CLIENT.get_or_init(|| RwLock::new(None)) -} +static DISCORD_CLIENT: LazyLock>> = + LazyLock::new(|| Mutex::new(None)); // Shared place to store the user ID of the authenticated user. -pub fn current_user_id() -> &'static RwLock> { - static USER_ID: OnceLock>> = OnceLock::new(); - USER_ID.get_or_init(|| RwLock::new(None)) -} +pub static CURRENT_USER_ID: LazyLock>> = LazyLock::new(|| RwLock::new(None)); + +// Flag to avoid multiple concurrent reconnect attempts. +static RECONNECTING: AtomicBool = AtomicBool::new(false); + +pub static AUDIO_INPUT_TYPE: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +pub static AUDIO_OUTPUT_TYPE: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); // Shared place to store the currently selected voice channel ID. -pub fn current_voice_channel() -> &'static RwLock> { - static CHANNEL: OnceLock>> = OnceLock::new(); - CHANNEL.get_or_init(|| RwLock::new(None)) +pub static CURRENT_VOICE_CHANNEL: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +// Last-known voice mode from Discord, updated via RPC events. +pub static CURRENT_VOICE_MODE: LazyLock>> = + LazyLock::new(|| RwLock::new(None)); + +pub static USER_VOICE_SETTINGS_MAP: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +// Locks the Discord client and returns the guard, or shows an alert and returns None if not initialized. +pub async fn get_discord_client( + instance: &Instance, +) -> OpenActionResult>> { + let guard = DISCORD_CLIENT.lock().await; + if guard.is_none() { + log::error!("Discord client not initialized"); + instance.show_alert().await?; + return Ok(None); + } + Ok(Some(MutexGuard::map(guard, |opt| opt.as_mut().unwrap()))) +} + +pub async fn get_audio_device_settings( + instance: &Instance, + device_type: &AudioDeviceType, +) -> OpenActionResult> { + let device = match device_type { + AudioDeviceType::Input => &AUDIO_INPUT_TYPE, + AudioDeviceType::Output => &AUDIO_OUTPUT_TYPE, + } + .read() + .await + .clone(); + + let Some(device) = device else { + log::error!( + "Failed to obtain voice settings for {:?} device", + device_type + ); + instance.show_alert().await?; + return Ok(None); + }; + + Ok(Some(device)) } // Store the latest error message in the global settings so the UI can surface it. pub async fn update_error(error: &str) { - let mut current = current_settings().write().await; + let mut current = CURRENT_SETTINGS.write().await; if current.error.as_deref() == Some(error) { return; } @@ -45,22 +94,16 @@ pub async fn update_error(error: &str) { } } -// Flag to avoid multiple concurrent reconnect attempts. -fn reconnecting_flag() -> &'static AtomicBool { - static RECONNECTING: OnceLock = OnceLock::new(); - RECONNECTING.get_or_init(|| AtomicBool::new(false)) -} - // Attempts to reinitialize the Discord IPC client using the stored settings. async fn reinitialize() { - let settings = current_settings().read().await.clone(); + let settings = CURRENT_SETTINGS.read().await.clone(); match create_discord_client(&settings).await { Ok(client) => { - *discord_client().write().await = Some(client); - reconnecting_flag().store(false, Ordering::SeqCst); + *DISCORD_CLIENT.lock().await = Some(client); + RECONNECTING.store(false, Ordering::SeqCst); } Err(e) => { - *discord_client().write().await = None; + *DISCORD_CLIENT.lock().await = None; log::error!("Failed to reinitialize client: {}", e); update_error(&e).await; } @@ -69,13 +112,12 @@ async fn reinitialize() { // Schedules periodic reconnect attempts until successful. pub(crate) fn schedule_reconnect() { - let flag = reconnecting_flag(); - if flag.swap(true, Ordering::SeqCst) { + if RECONNECTING.swap(true, Ordering::SeqCst) { return; } - tokio::spawn(async move { - while flag.load(Ordering::SeqCst) { + tokio::spawn(async { + while RECONNECTING.load(Ordering::SeqCst) { reinitialize().await; sleep(Duration::from_secs(5)).await; } @@ -83,7 +125,7 @@ pub(crate) fn schedule_reconnect() { } pub async fn update_voice_state_subscription(channel_id: String, subscribe: bool) { - let mut client_lock = discord_client().write().await; + let mut client_lock = DISCORD_CLIENT.lock().await; let Some(client) = client_lock.as_mut() else { log::error!("Discord client not initialized"); return; @@ -184,7 +226,7 @@ async fn setup_discord_client( .await .map_err(|e| format!("Failed to fetch soundboard sounds: {}", e))?; - let mut current = current_settings().write().await; + let mut current = CURRENT_SETTINGS.write().await; current.error = None; if let Err(e) = set_global_settings(&*current).await { log::error!("Failed to clear error: {}", e); @@ -204,7 +246,7 @@ async fn create_discord_client(settings: &DiscordSettings) -> Result Result { log::info!("Successfully obtained access token"); - let mut current = current_settings().write().await; + let mut current = CURRENT_SETTINGS.write().await; current.access_token = access_token.clone(); if let Err(e) = set_global_settings(&*current).await { log::error!("Failed to save access token: {}", e); } drop(current); - let mut client_lock = discord_client().write().await; + let mut client_lock = DISCORD_CLIENT.lock().await; let Some(client) = client_lock.as_mut() else { log::error!("Discord client not initialized"); return; diff --git a/src/main.rs b/src/main.rs index 23326f0..be6d643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod actions; +mod audio_device_utils; mod cache; mod client; mod oauth; @@ -7,7 +8,7 @@ mod rpc_events; use actions::*; use client::schedule_reconnect; -use std::sync::OnceLock; +use std::sync::LazyLock; use openaction::{ OpenActionResult, async_trait, get_global_settings, global_events, register_action, run, @@ -29,10 +30,8 @@ pub struct DiscordSettings { } // Global storage for the last-applied settings so every module can read/write them. -pub fn current_settings() -> &'static RwLock { - static SETTINGS: OnceLock> = OnceLock::new(); - SETTINGS.get_or_init(|| RwLock::new(DiscordSettings::default())) -} +pub static CURRENT_SETTINGS: LazyLock> = + LazyLock::new(|| RwLock::new(DiscordSettings::default())); // Handles global setting updates pushed from the Stream Deck host. pub struct GlobalEventHandler; @@ -50,7 +49,7 @@ impl global_events::GlobalEventHandler for GlobalEventHandler { serde_json::from_value(event.payload.settings).unwrap_or_default(); // Only react when the stored settings actually changed so we can avoid reconnect churn. - let current = current_settings().read().await; + let current = CURRENT_SETTINGS.read().await; let settings_changed = current.client_id != settings.client_id || current.client_secret != settings.client_secret || current.access_token != settings.access_token @@ -63,7 +62,7 @@ impl global_events::GlobalEventHandler for GlobalEventHandler { log::info!("Global settings changed, reinitializing Discord client"); // Persist the new configuration before attempting to reconnect. - *current_settings().write().await = settings; + *CURRENT_SETTINGS.write().await = settings; schedule_reconnect(); } diff --git a/src/rpc_events.rs b/src/rpc_events.rs index b0efed5..4962899 100644 --- a/src/rpc_events.rs +++ b/src/rpc_events.rs @@ -1,8 +1,9 @@ -use crate::actions::audio_device_utils::{ - AudioDeviceType, AudioDeviceWrapper, user_voice_settings_map, +use crate::CURRENT_SETTINGS; +use crate::audio_device_utils::{AudioDeviceType, AudioDeviceWrapper}; +use crate::client::{ + AUDIO_INPUT_TYPE, AUDIO_OUTPUT_TYPE, CURRENT_USER_ID, CURRENT_VOICE_CHANNEL, + CURRENT_VOICE_MODE, USER_VOICE_SETTINGS_MAP, schedule_reconnect, }; -use crate::client::{current_user_id, current_voice_channel, schedule_reconnect}; -use crate::current_settings; use discord_ipc_rust::models::receive::events::NotificationCreateData; use discord_ipc_rust::models::receive::{ @@ -21,7 +22,7 @@ pub async fn handle_rpc_event(item: ReceivedItem) { error.message ); if error.code == 4006 { - let mut current = current_settings().write().await; + let mut current = CURRENT_SETTINGS.write().await; current.access_token.clear(); if let Err(e) = set_global_settings(&*current).await { log::error!("Failed to clear access token in settings: {}", e); @@ -35,10 +36,10 @@ pub async fn handle_rpc_event(item: ReceivedItem) { return; }; - let current_user_id = current_user_id().read().await; + let current_user_id = CURRENT_USER_ID.read().await; if current_user_id.as_ref() != Some(&user.id) { - user_voice_settings_map() + USER_VOICE_SETTINGS_MAP .write() .await .insert(user.id.clone(), state.into()); @@ -52,7 +53,7 @@ pub async fn handle_rpc_event(item: ReceivedItem) { } ReturnedEvent::VoiceStateDelete(state) => { if let Some(user) = &state.user { - user_voice_settings_map().write().await.remove(&user.id); + USER_VOICE_SETTINGS_MAP.write().await.remove(&user.id); for instance in visible_instances(crate::actions::UserVolumeControlAction::UUID).await @@ -99,15 +100,15 @@ pub async fn handle_rpc_event(item: ReceivedItem) { }, ReceivedItem::SocketClosed => { log::warn!("Discord closed; attempting to reconnect"); - crate::cache::guild_cache().write().await.clear(); - user_voice_settings_map().write().await.clear(); + crate::cache::GUILD_CACHE.write().await.clear(); + USER_VOICE_SETTINGS_MAP.write().await.clear(); schedule_reconnect(); } } } async fn handle_select_voice_channel(channel_id: Option) { - let old_channel = current_voice_channel().read().await.clone(); + let old_channel = CURRENT_VOICE_CHANNEL.read().await.clone(); if old_channel == channel_id { return; @@ -115,10 +116,10 @@ async fn handle_select_voice_channel(channel_id: Option) { if let Some(old_channel) = old_channel { crate::client::update_voice_state_subscription(old_channel, false).await; - user_voice_settings_map().write().await.clear(); + USER_VOICE_SETTINGS_MAP.write().await.clear(); } - *current_voice_channel().write().await = channel_id.clone(); + *CURRENT_VOICE_CHANNEL.write().await = channel_id.clone(); if let Some(new_channel) = channel_id { crate::client::update_voice_state_subscription(new_channel, true).await; @@ -145,7 +146,7 @@ async fn apply_voice_state(settings: discord_ipc_rust::models::shared::voice::Vo if let Some(mode) = &settings.mode { let is_ptt = mode.mode_type == "PUSH_TO_TALK"; update_action_state(crate::actions::ToggleVoiceInputModeAction::UUID, is_ptt).await; - *crate::actions::current_voice_mode().write().await = + *CURRENT_VOICE_MODE.write().await = Some(discord_ipc_rust::models::shared::voice::VoiceSettingsMode { mode_type: mode.mode_type.clone(), ..*mode @@ -153,9 +154,7 @@ async fn apply_voice_state(settings: discord_ipc_rust::models::shared::voice::Vo } if let Some(input) = settings.input { - *crate::actions::audio_device_utils::audio_input_settings() - .write() - .await = Some(AudioDeviceWrapper { + *AUDIO_INPUT_TYPE.write().await = Some(AudioDeviceWrapper { device_type: AudioDeviceType::Input, device_id: input.device_id, volume: input.volume, @@ -164,9 +163,7 @@ async fn apply_voice_state(settings: discord_ipc_rust::models::shared::voice::Vo } if let Some(output) = settings.output { - *crate::actions::audio_device_utils::audio_output_settings() - .write() - .await = Some(AudioDeviceWrapper { + *AUDIO_OUTPUT_TYPE.write().await = Some(AudioDeviceWrapper { device_type: AudioDeviceType::Output, device_id: output.device_id, volume: output.volume,