From 2080642e6c77f67c2cf59786bd274871ef22dec0 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Fri, 10 Oct 2025 23:41:53 +0530 Subject: [PATCH 01/62] dev: wrapped libc::getuid into a function --- src/ipc/connection.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 21639d6..49f2bbc 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -84,6 +84,12 @@ impl IpcConnection { } } + // Returns the current users UID on unix based systems + #[cfg(unix)] + fn current_uid() -> u32 { + unsafe { libc::getuid() } + } + #[cfg(unix)] fn discover_pipes_unix() -> Vec { let mut pipes = Vec::new(); @@ -105,7 +111,7 @@ impl IpcConnection { // Fallback to /run/user/{uid} if no env vars found if directories.is_empty() { - let uid = unsafe { libc::getuid() }; + let uid = Self::current_uid(); directories.push(format!("/run/user/{}", uid)); // Also try Flatpak path as fallback directories.push(format!("/run/user/{}/app/com.discordapp.Discord", uid)); From fdaf55db2d96b2850706f7b5c5819dd9b30f9f93 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 11 Oct 2025 00:06:03 +0530 Subject: [PATCH 02/62] dev: refactored discover pipes --- src/ipc/connection.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 49f2bbc..f01cb1d 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -89,27 +89,29 @@ impl IpcConnection { fn current_uid() -> u32 { unsafe { libc::getuid() } } - + /// Discovers potential base directories where IPC sockets may exist + /// Check enviroment variables + /// - `XDG_RUNTIME_DIR` + /// - `TMPDIR` + /// - `TMP` + /// - `TEMP` + /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specfic + /// if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user + /// - `/run/user/{UID}` #[cfg(unix)] - fn discover_pipes_unix() -> Vec { - let mut pipes = Vec::new(); - - // Try environment variables in order of preference + fn candidate_ipc_dir() -> Vec { let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"]; let mut directories = Vec::new(); - - for env_key in &env_keys { - if let Ok(dir) = std::env::var(env_key) { + for key in &env_keys { + if let Ok(dir) = std::env::var(key) { directories.push(dir.clone()); // Also check Flatpak Discord path if XDG_RUNTIME_DIR is set - if env_key == &"XDG_RUNTIME_DIR" { + if key == &"XDG_RUNTIME_DIR" { directories.push(format!("{}/app/com.discordapp.Discord", dir)); } } } - - // Fallback to /run/user/{uid} if no env vars found if directories.is_empty() { let uid = Self::current_uid(); directories.push(format!("/run/user/{}", uid)); @@ -117,8 +119,14 @@ impl IpcConnection { directories.push(format!("/run/user/{}/app/com.discordapp.Discord", uid)); } + directories + } + #[cfg(unix)] + fn discover_pipes_unix() -> Vec { + let mut pipes = Vec::new(); + // Try each directory with each socket number - for dir in &directories { + for dir in Self::candidate_ipc_dir() { for i in 0..constants::MAX_IPC_SOCKETS { let socket_path = format!("{}/{}{}", dir, constants::IPC_SOCKET_PREFIX, i); From c3ac3aaae3e07cf60fed5a9dd2ca8c0b9a1d3851 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 11 Oct 2025 00:20:39 +0530 Subject: [PATCH 03/62] dev: refactored unix auto connect --- src/ipc/connection.rs | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index f01cb1d..aa15342 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -298,34 +298,11 @@ impl IpcConnection { #[cfg(unix)] /// Connect to Discord IPC socket using auto-discovery fn connect_to_discord_unix_auto() -> Result { - // Try environment variables in order of preference - let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"]; - let mut directories = Vec::new(); - - for env_key in &env_keys { - if let Ok(dir) = std::env::var(env_key) { - directories.push(dir.clone()); - - // Also check Flatpak Discord path if XDG_RUNTIME_DIR is set - if env_key == &"XDG_RUNTIME_DIR" { - directories.push(format!("{}/app/com.discordapp.Discord", dir)); - } - } - } - - // Fallback to /run/user/{uid} if no env vars found - if directories.is_empty() { - let uid = unsafe { libc::getuid() }; - directories.push(format!("/run/user/{}", uid)); - // Also try Flatpak path as fallback - directories.push(format!("/run/user/{}/app/com.discordapp.Discord", uid)); - } - // Try each directory with each socket number let mut last_error = None; let mut attempted_paths = Vec::new(); - for dir in &directories { + for dir in Self::candidate_ipc_dir() { for i in 0..constants::MAX_IPC_SOCKETS { let socket_path = format!("{}/{}{}", dir, constants::IPC_SOCKET_PREFIX, i); attempted_paths.push(socket_path.clone()); From be8432fed296b62248a973b3e8ccfdeb9ee661cb Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 11 Oct 2025 00:36:51 +0530 Subject: [PATCH 04/62] dev: merged retry tests to a single file --- src/retry.rs | 95 +++++++++++++++++++++++++++++++++++++++++++- src/retry/tests.rs | 99 ---------------------------------------------- 2 files changed, 94 insertions(+), 100 deletions(-) delete mode 100644 src/retry/tests.rs diff --git a/src/retry.rs b/src/retry.rs index 5baf52c..abd81d4 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -232,4 +232,97 @@ where } #[cfg(test)] -mod tests; +#[test] +fn test_retry_config_creation() { + let config = RetryConfig::default(); + assert_eq!(config.max_attempts, 3); + assert_eq!(config.initial_delay_ms, 1000); + + let custom = RetryConfig::with_max_attempts(5); + assert_eq!(custom.max_attempts, 5); +} + +#[test] +fn test_retry_config_delay_calculation() { + let config = RetryConfig::new(5, 1000, 10000, 2.0); + + // Test exponential backoff + let delay0 = config.delay_for_attempt(0); + let delay1 = config.delay_for_attempt(1); + let delay2 = config.delay_for_attempt(2); + + assert_eq!(delay0.as_millis(), 1000); + assert_eq!(delay1.as_millis(), 2000); + assert_eq!(delay2.as_millis(), 4000); +} + +#[test] +fn test_retry_config_max_delay() { + let config = RetryConfig::new(10, 1000, 5000, 2.0); + + // Delay should cap at max_delay_ms + let delay10 = config.delay_for_attempt(10); + assert_eq!(delay10.as_millis(), 5000); +} + +#[test] +fn test_retry_exhausts_attempts() { + let config = RetryConfig::with_max_attempts(3); + + let mut attempt_count = 0; + let result: std::result::Result<(), DiscordIpcError> = with_retry(&config, || { + attempt_count += 1; + // SocketClosed is recoverable + Err(DiscordIpcError::SocketClosed) + }); + + assert!(result.is_err()); + assert_eq!(attempt_count, 3); +} + +#[test] +fn test_non_recoverable_error_no_retry() { + let config = RetryConfig::with_max_attempts(3); + + let mut attempt_count = 0; + let result: std::result::Result<(), DiscordIpcError> = with_retry(&config, || { + attempt_count += 1; + // ConnectionFailed is NOT recoverable + Err(DiscordIpcError::ConnectionFailed(std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + "test", + ))) + }); + + assert!(result.is_err()); + assert_eq!(attempt_count, 1); // Should NOT retry +} + +#[test] +fn test_retry_succeeds_on_first_attempt() { + let config = RetryConfig::with_max_attempts(3); + + let mut attempt_count = 0; + let result = with_retry(&config, || { + attempt_count += 1; + Ok::<_, DiscordIpcError>(42) + }); + + assert_eq!(result.unwrap(), 42); + assert_eq!(attempt_count, 1); +} + +#[test] +fn test_retry_stops_on_non_recoverable_error() { + let config = RetryConfig::with_max_attempts(5); + + let mut attempt_count = 0; + let result: std::result::Result<(), DiscordIpcError> = with_retry(&config, || { + attempt_count += 1; + // InvalidActivity is not recoverable + Err(DiscordIpcError::InvalidActivity("test".to_string())) + }); + + assert!(result.is_err()); + assert_eq!(attempt_count, 1); // Should fail immediately +} diff --git a/src/retry/tests.rs b/src/retry/tests.rs deleted file mode 100644 index e7f1c00..0000000 --- a/src/retry/tests.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::error::DiscordIpcError; -/// Smoke test for connection retry functionality -/// This test verifies that the retry module compiles and the API works correctly -use crate::retry::{with_retry, RetryConfig}; - -#[test] -fn test_retry_config_creation() { - let config = RetryConfig::default(); - assert_eq!(config.max_attempts, 3); - assert_eq!(config.initial_delay_ms, 1000); - - let custom = RetryConfig::with_max_attempts(5); - assert_eq!(custom.max_attempts, 5); -} - -#[test] -fn test_retry_config_delay_calculation() { - let config = RetryConfig::new(5, 1000, 10000, 2.0); - - // Test exponential backoff - let delay0 = config.delay_for_attempt(0); - let delay1 = config.delay_for_attempt(1); - let delay2 = config.delay_for_attempt(2); - - assert_eq!(delay0.as_millis(), 1000); - assert_eq!(delay1.as_millis(), 2000); - assert_eq!(delay2.as_millis(), 4000); -} - -#[test] -fn test_retry_config_max_delay() { - let config = RetryConfig::new(10, 1000, 5000, 2.0); - - // Delay should cap at max_delay_ms - let delay10 = config.delay_for_attempt(10); - assert_eq!(delay10.as_millis(), 5000); -} - -#[test] -fn test_retry_exhausts_attempts() { - let config = RetryConfig::with_max_attempts(3); - - let mut attempt_count = 0; - let result: Result<(), DiscordIpcError> = with_retry(&config, || { - attempt_count += 1; - // SocketClosed is recoverable - Err(DiscordIpcError::SocketClosed) - }); - - assert!(result.is_err()); - assert_eq!(attempt_count, 3); -} - -#[test] -fn test_non_recoverable_error_no_retry() { - let config = RetryConfig::with_max_attempts(3); - - let mut attempt_count = 0; - let result: Result<(), DiscordIpcError> = with_retry(&config, || { - attempt_count += 1; - // ConnectionFailed is NOT recoverable - Err(DiscordIpcError::ConnectionFailed(std::io::Error::new( - std::io::ErrorKind::ConnectionRefused, - "test", - ))) - }); - - assert!(result.is_err()); - assert_eq!(attempt_count, 1); // Should NOT retry -} - -#[test] -fn test_retry_succeeds_on_first_attempt() { - let config = RetryConfig::with_max_attempts(3); - - let mut attempt_count = 0; - let result = with_retry(&config, || { - attempt_count += 1; - Ok::<_, DiscordIpcError>(42) - }); - - assert_eq!(result.unwrap(), 42); - assert_eq!(attempt_count, 1); -} - -#[test] -fn test_retry_stops_on_non_recoverable_error() { - let config = RetryConfig::with_max_attempts(5); - - let mut attempt_count = 0; - let result: Result<(), DiscordIpcError> = with_retry(&config, || { - attempt_count += 1; - // InvalidActivity is not recoverable - Err(DiscordIpcError::InvalidActivity("test".to_string())) - }); - - assert!(result.is_err()); - assert_eq!(attempt_count, 1); // Should fail immediately -} From 3f70a02193284f3b7027537572e6baca79e1c32f Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 11 Oct 2025 00:49:31 +0530 Subject: [PATCH 05/62] dev: unfied async retry api --- src/ipc/connection.rs | 2 +- src/retry.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index aa15342..533a9d4 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -96,7 +96,7 @@ impl IpcConnection { /// - `TMP` /// - `TEMP` /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specfic - /// if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user + /// - if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user /// - `/run/user/{UID}` #[cfg(unix)] fn candidate_ipc_dir() -> Vec { diff --git a/src/retry.rs b/src/retry.rs index abd81d4..1873acc 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -139,8 +139,10 @@ where /// # Ok(()) /// # } /// ``` +/// Retry an async operation with exponential backoff (unified API) + #[cfg(feature = "tokio-runtime")] -pub async fn with_retry_async(config: &RetryConfig, mut operation: F) -> Result +pub async fn with_retry_async_tokio(config: &RetryConfig, mut operation: F) -> Result where F: FnMut() -> Fut, Fut: std::future::Future>, @@ -228,9 +230,18 @@ where std::io::ErrorKind::Other, "Retry attempts exhausted", )) - })) + })); } +#[cfg(feature = "tokio-runtime")] +pub use with_retry_async_tokio as with_retry_async; + +#[cfg(feature = "async-std-runtime")] +pub use with_retry_async_std as with_retry_async; + +#[cfg(feature = "smol-runtime")] +pub use with_retry_async_smol as with_retry_async; + #[cfg(test)] #[test] fn test_retry_config_creation() { From 98b0bea9e6763f19c51cb0f48a63d2c2f52f92ae Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 11 Oct 2025 18:45:36 +0530 Subject: [PATCH 06/62] docs: fixed mutliple named refrences --- src/retry.rs | 155 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 12 deletions(-) diff --git a/src/retry.rs b/src/retry.rs index 1873acc..0559fdd 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -114,9 +114,9 @@ where })) } -/// Retry an async operation with exponential backoff +/// Retry an async operation with exponential backoff (Tokio runtime) /// -/// This is the async version of `with_retry`. +/// This function uses `tokio::time::sleep` for delays between retry attempts. /// /// # Arguments /// @@ -127,20 +127,23 @@ where /// /// ```no_run /// use presenceforge::async_io::tokio::TokioDiscordIpcClient; -/// use presenceforge::retry::{with_retry_async, RetryConfig}; +/// use presenceforge::retry::{with_retry_async_tokio, RetryConfig}; /// /// # #[tokio::main] /// # async fn main() -> Result<(), presenceforge::DiscordIpcError> { /// let config = RetryConfig::with_max_attempts(5); /// -/// let mut client = with_retry_async(&config, || { +/// let mut client = with_retry_async_tokio(&config, || { /// Box::pin(async { TokioDiscordIpcClient::new("your-client-id").await }) /// }).await?; /// # Ok(()) /// # } /// ``` -/// Retry an async operation with exponential backoff (unified API) - +/// +/// # Note +/// +/// When the `tokio-runtime` feature is enabled, this function is also exported as +/// [`with_retry_async`] for convenience (with priority over other runtimes). #[cfg(feature = "tokio-runtime")] pub async fn with_retry_async_tokio(config: &RetryConfig, mut operation: F) -> Result where @@ -171,7 +174,35 @@ where })) } -/// Retry an async operation with exponential backoff (async-std version) +/// Retry an async operation with exponential backoff (async-std runtime) +/// +/// This function uses `async_std::task::sleep` for delays between retry attempts. +/// +/// # Arguments +/// +/// * `config` - Retry configuration +/// * `operation` - The async operation to retry +/// +/// # Example +/// +/// ```no_run +/// use presenceforge::async_io::async_std::AsyncStdDiscordIpcClient; +/// use presenceforge::retry::{with_retry_async_std, RetryConfig}; +/// +/// # async_std::task::block_on(async { +/// let config = RetryConfig::with_max_attempts(5); +/// +/// let mut client = with_retry_async_std(&config, || { +/// Box::pin(async { AsyncStdDiscordIpcClient::new("your-client-id").await }) +/// }).await?; +/// # Ok::<(), presenceforge::DiscordIpcError>(()) +/// # }); +/// ``` +/// +/// # Note +/// +/// When the `async-std-runtime` feature is enabled (and `tokio-runtime` is not), +/// this function is also exported as [`with_retry_async`] for convenience. #[cfg(feature = "async-std-runtime")] pub async fn with_retry_async_std(config: &RetryConfig, mut operation: F) -> Result where @@ -202,7 +233,36 @@ where })) } -/// Retry an async operation with exponential backoff (smol version) +/// Retry an async operation with exponential backoff (smol runtime) +/// +/// This function uses `smol::Timer::after` for delays between retry attempts. +/// +/// # Arguments +/// +/// * `config` - Retry configuration +/// * `operation` - The async operation to retry +/// +/// # Example +/// +/// ```no_run +/// use presenceforge::async_io::smol::SmolDiscordIpcClient; +/// use presenceforge::retry::{with_retry_async_smol, RetryConfig}; +/// +/// # smol::block_on(async { +/// let config = RetryConfig::with_max_attempts(5); +/// +/// let mut client = with_retry_async_smol(&config, || { +/// Box::pin(async { SmolDiscordIpcClient::new("your-client-id").await }) +/// }).await?; +/// # Ok::<(), presenceforge::DiscordIpcError>(()) +/// # }); +/// ``` +/// +/// # Note +/// +/// When the `smol-runtime` feature is enabled (and neither `tokio-runtime` nor +/// `async-std-runtime` is enabled), this function is also exported as +/// [`with_retry_async`] for convenience. #[cfg(feature = "smol-runtime")] pub async fn with_retry_async_smol(config: &RetryConfig, mut operation: F) -> Result where @@ -230,16 +290,87 @@ where std::io::ErrorKind::Other, "Retry attempts exhausted", )) - })); + })) } -#[cfg(feature = "tokio-runtime")] +// Unified async retry API +// +// The `with_retry_async` function is an alias to the appropriate runtime-specific +// retry function based on which feature is enabled: +// - tokio-runtime -> with_retry_async_tokio +// - async-std-runtime -> with_retry_async_std (if tokio not enabled) +// - smol-runtime -> with_retry_async_smol (if others not enabled) + +/// Retry an async operation with exponential backoff (unified API) +/// +/// This is a convenience alias that automatically uses the correct retry implementation +/// based on your enabled async runtime feature. It provides a unified API regardless of +/// which runtime you're using. +/// +/// # Runtime Selection +/// +/// - **tokio-runtime**: Uses [`with_retry_async_tokio`] (priority if multiple features enabled) +/// - **async-std-runtime**: Uses [`with_retry_async_std`] (if tokio not enabled) +/// - **smol-runtime**: Uses [`with_retry_async_smol`] (if others not enabled) +/// +/// # Arguments +/// +/// * `config` - Retry configuration +/// * `operation` - The async operation to retry +/// +/// # Examples +/// +/// With Tokio: +/// ```no_run +/// use presenceforge::async_io::tokio::TokioDiscordIpcClient; +/// use presenceforge::retry::{with_retry_async, RetryConfig}; +/// +/// # #[tokio::main] +/// # async fn main() -> Result<(), presenceforge::DiscordIpcError> { +/// let config = RetryConfig::with_max_attempts(5); +/// let client = with_retry_async(&config, || { +/// Box::pin(async { TokioDiscordIpcClient::new("your-client-id").await }) +/// }).await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// With async-std: +/// ```no_run +/// use presenceforge::async_io::async_std::AsyncStdDiscordIpcClient; +/// use presenceforge::retry::{with_retry_async, RetryConfig}; +/// +/// # async_std::task::block_on(async { +/// let config = RetryConfig::with_max_attempts(5); +/// let client = with_retry_async(&config, || { +/// Box::pin(async { AsyncStdDiscordIpcClient::new("your-client-id").await }) +/// }).await?; +/// # Ok::<(), presenceforge::DiscordIpcError>(()) +/// # }); +/// ``` +#[cfg(all( + feature = "tokio-runtime", + not(all(feature = "async-std-runtime", not(feature = "tokio-runtime"))), + not(all(feature = "smol-runtime", not(feature = "tokio-runtime"))) +))] pub use with_retry_async_tokio as with_retry_async; -#[cfg(feature = "async-std-runtime")] +/// Retry an async operation with exponential backoff (unified API) +/// +/// This is a convenience alias to [`with_retry_async_std`] when using the async-std runtime. +/// See the [`with_retry_async`](with_retry_async_tokio) documentation for more details. +#[cfg(all(feature = "async-std-runtime", not(feature = "tokio-runtime")))] pub use with_retry_async_std as with_retry_async; -#[cfg(feature = "smol-runtime")] +/// Retry an async operation with exponential backoff (unified API) +/// +/// This is a convenience alias to [`with_retry_async_smol`] when using the smol runtime. +/// See the [`with_retry_async`](with_retry_async_tokio) documentation for more details. +#[cfg(all( + feature = "smol-runtime", + not(feature = "tokio-runtime"), + not(feature = "async-std-runtime") +))] pub use with_retry_async_smol as with_retry_async; #[cfg(test)] From 0073910dc540c443cc04ac0e73e3293e16680ac5 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sun, 12 Oct 2025 14:59:56 +0530 Subject: [PATCH 07/62] changelogs: added unrealsed changelogs --- changelogs/unreleased.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 changelogs/unreleased.md diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md new file mode 100644 index 0000000..d82463f --- /dev/null +++ b/changelogs/unreleased.md @@ -0,0 +1,35 @@ +# Unreleased + +## [0.0.0] - Unreleased + +> **WARNING:** Early development version. Not recommended for production use. + +### Added + +#### Core Features + +- Cross-platform Discord IPC support (Unix sockets for Linux/macOS, named pipes for Windows) +- Automatic discovery of Discord IPC pipes/sockets +- Flatpak Discord support with automatic detection +- Synchronous client API (`DiscordIpcClient`) +- Unified async API (`AsyncDiscordIpcClient`) with runtime-agnostic design +- Support for Tokio, async-std, and smol runtimes via feature flags +- Activity builder pattern for creating Rich Presence activities +- Full Discord Rich Presence field support (state, details, timestamps, assets, buttons, party, secrets) +- Input validation for all Discord field length limits +- Pipe discovery and custom path selection +- Connection timeout configuration +- Retry logic with exponential backoff (sync and async) +- Comprehensive error handling with categorization +- UUID v4-based cryptographic nonces for request tracking + +#### Testing + +- Unit tests for core functionality +- Integration tests for activity builder, serialization, error handling, IPC protocol, and retry logic + +### Feature Flags + +- `tokio-runtime` - Tokio async runtime support +- `async-std-runtime` - async-std runtime support +- `smol-runtime` - smol runtime support From df8402e696d8256ddb0b60083dcf0cc800750cdf Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Mon, 13 Oct 2025 19:19:45 +0530 Subject: [PATCH 08/62] examples: added example for tokio - this was added to show how to update an activity without reseting the timer --- changelogs/unreleased.md | 4 +- examples/README.md | 15 +++ examples/update_activity_tokio.rs | 172 ++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 examples/update_activity_tokio.rs diff --git a/changelogs/unreleased.md b/changelogs/unreleased.md index d82463f..b2acb39 100644 --- a/changelogs/unreleased.md +++ b/changelogs/unreleased.md @@ -15,8 +15,8 @@ - Unified async API (`AsyncDiscordIpcClient`) with runtime-agnostic design - Support for Tokio, async-std, and smol runtimes via feature flags - Activity builder pattern for creating Rich Presence activities -- Full Discord Rich Presence field support (state, details, timestamps, assets, buttons, party, secrets) -- Input validation for all Discord field length limits +- Full Discord Rich Presence field support (state, details, timestamps, assets, buttons, party) +- Basic input validation for all Discord field length limits - Pipe discovery and custom path selection - Connection timeout configuration - Retry logic with exponential backoff (sync and async) diff --git a/examples/README.md b/examples/README.md index aaab409..fa59623 100644 --- a/examples/README.md +++ b/examples/README.md @@ -72,6 +72,9 @@ cargo run --example connection_retry -- --client-id YOUR_CLIENT_ID # Async Tokio reconnect - Connection retry with Tokio async runtime cargo run --example async_tokio_reconnect --features tokio-runtime -- --client-id YOUR_CLIENT_ID +# Update activity with Tokio - Update state/details without resetting the timer +cargo run --example update_activity_tokio --features tokio-runtime -- --client-id YOUR_CLIENT_ID + # Flatpak Discord - Connect to Flatpak Discord using custom path configuration cargo run --example flatpak_discord -- --client-id YOUR_CLIENT_ID ``` @@ -199,6 +202,18 @@ Async error handling with Tokio showing: - Resilient connection loop with exponential backoff - Async error recovery patterns +### `update_activity_tokio.rs` + +Activity state updates without timer reset (Tokio async) showing: + +- **Updating activity state/details while keeping the same timer** +- Using a consistent `start_timestamp` across all updates +- Demonstrating multiple state changes (main menu → in game → multiplayer → loading → back to menu) +- Perfect for games or apps that need to update status without resetting elapsed time +- Shows how to maintain session continuity across state changes + +**Key Feature:** The elapsed time on Discord continues uninterrupted when you update the activity while maintaining the original timestamp! + ### `flatpak_discord.rs` Flatpak Discord example showing: diff --git a/examples/update_activity_tokio.rs b/examples/update_activity_tokio.rs new file mode 100644 index 0000000..5750326 --- /dev/null +++ b/examples/update_activity_tokio.rs @@ -0,0 +1,172 @@ +use clap::Parser; +#[cfg(feature = "tokio-runtime")] +use presenceforge::{ActivityBuilder, AsyncDiscordIpcClient, Result}; +#[cfg(feature = "tokio-runtime")] +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Discord Rich Presence - Update Activity Without Resetting Timer +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Discord Application Client ID + #[arg(short, long)] + client_id: Option, +} +#[cfg(feature = "tokio-runtime")] +#[tokio::main] +async fn main() -> Result { + // Load .env file if it exists (optional) + let _ = dotenvy::dotenv(); + + let args = Args::parse(); + + let client_id = args + .client_id + .or_else(|| std::env::var("DISCORD_CLIENT_ID").ok()) + .unwrap_or_else(|| { + eprintln!("Error: DISCORD_CLIENT_ID is required!"); + eprintln!("Provide it via:"); + eprintln!(" - Command line: cargo run --example update_activity_tokio --features tokio-runtime -- --client-id YOUR_ID"); + eprintln!(" - Environment: DISCORD_CLIENT_ID=YOUR_ID cargo run --example update_activity_tokio --features tokio-runtime"); + eprintln!(" - .env file: Create .env from .env.example and set DISCORD_CLIENT_ID"); + std::process::exit(1); + }); + + let mut client = AsyncDiscordIpcClient::new(&client_id).await?; + + // Perform handshake + client.connect().await?; + println!("✓ Connected to Discord!"); + + // Get the initial timestamp - this will stay consistent across all updates + let start_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + println!("\n=== Demonstrating Activity Updates Without Timer Reset ===\n"); + println!("The timer will remain consistent throughout all state changes!\n"); + + // Initial activity - Main Menu + println!(" State 1: Main Menu"); + let activity = ActivityBuilder::new() + .state("Browsing menus") + .details("In main menu") + .start_timestamp(start_timestamp) + .large_image("game") + .large_text("Game Icon") + .small_image("idle") + .small_text("Idle") + .button(" Play Game", "https://example.com") + .build(); + + client.set_activity(&activity).await?; + println!("✓ Activity set: In main menu"); + println!(" Timer started at: {}", start_timestamp); + + // Wait 5 seconds + println!("\n Waiting 5 seconds...\n"); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Update to In Game - SAME TIMESTAMP + println!(" State 2: In Game"); + let activity = ActivityBuilder::new() + .state("Playing Solo") + .details("In game - Level 5") + .start_timestamp(start_timestamp) // SAME timestamp = timer continues! + .large_image("game") + .large_text("Game Icon") + .small_image("playing") + .small_text("Playing") + .button("🎮 Play Game", "https://example.com") + .build(); + + client.set_activity(&activity).await?; + println!("✓ Activity updated: In game"); + println!(" Timer continues from: {}", start_timestamp); + println!(" Notice: The elapsed time on Discord continues!"); + + // Wait 5 seconds + println!("\n Waiting 5 seconds...\n"); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Update to Multiplayer - SAME TIMESTAMP + println!(" State 3: Multiplayer"); + let activity = ActivityBuilder::new() + .state("In Multiplayer Match") + .details("Team Deathmatch - 2/8 players") + .start_timestamp(start_timestamp) // SAME timestamp = timer continues! + .large_image("game") + .large_text("Game Icon") + .small_image("multiplayer") + .small_text("Online") + .button(" Join Game", "https://example.com") + .build(); + + client.set_activity(&activity).await?; + println!("✓ Activity updated: Multiplayer"); + println!(" Timer continues from: {}", start_timestamp); + println!(" Notice: The elapsed time keeps going!"); + + // Wait 5 seconds + println!("\n Waiting 5 seconds...\n"); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Update to Loading - SAME TIMESTAMP + println!(" State 4: Loading"); + let activity = ActivityBuilder::new() + .state("Loading next map...") + .details("Please wait") + .start_timestamp(start_timestamp) // SAME timestamp = timer continues! + .large_image("game") + .large_text("Game Icon") + .small_image("loading") + .small_text("Loading") + .button("🎮 Play Game", "https://example.com") + .build(); + + client.set_activity(&activity).await?; + println!("✓ Activity updated: Loading"); + println!(" Timer continues from: {}", start_timestamp); + + // Wait 5 seconds + println!("\n Waiting 5 seconds...\n"); + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + + // Back to Main Menu - SAME TIMESTAMP + println!(" State 5: Back to Main Menu"); + let activity = ActivityBuilder::new() + .state("Browsing menus") + .details("In main menu") + .start_timestamp(start_timestamp) // SAME timestamp = timer continues! + .large_image("game") + .large_text("Game Icon") + .small_image("idle") + .small_text("Idle") + .button(" Play Game", "https://example.com") + .build(); + + client.set_activity(&activity).await?; + println!("✓ Activity updated: Back to main menu"); + println!(" Timer continues from: {}", start_timestamp); + println!(" Total elapsed time: ~20 seconds (5s × 4 intervals)"); + + // Keep showing for a bit longer + println!("\n Keeping presence active for 10 more seconds..."); + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + + println!("\n=== Summary ==="); + println!("✓ Changed activity 5 times"); + println!("✓ Timer remained consistent throughout all changes"); + println!("✓ Total session time: ~30 seconds"); + println!("\nKey takeaway: By using the same start_timestamp across all"); + println!("activity updates, the elapsed time continues without resetting!"); + + // Clear the activity + println!("\n Clearing activity..."); + client.clear_activity().await?; + println!("✓ Activity cleared!"); + + // Connection is automatically closed when client is dropped + Ok(()) +} From a68fa0ffeb7bb9a3b878a08817e2ecc7728a3984 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Tue, 14 Oct 2025 14:18:13 +0530 Subject: [PATCH 09/62] dev: fixed tokio examples --- examples/update_activity_tokio.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/update_activity_tokio.rs b/examples/update_activity_tokio.rs index 5750326..364bdfd 100644 --- a/examples/update_activity_tokio.rs +++ b/examples/update_activity_tokio.rs @@ -170,3 +170,8 @@ async fn main() -> Result { // Connection is automatically closed when client is dropped Ok(()) } +#[cfg(not(feature = "tokio-runtime"))] +fn main() { + eprintln!("This example requires the `tokio-runtime` feature."); + eprintln!("Run with: cargo run --example async_tokio_reconnect --features tokio-runtime"); +} From 5103a60dcd5a93366b41665ce98df9bef34fc271 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Tue, 14 Oct 2025 14:22:26 +0530 Subject: [PATCH 10/62] chore: fixed typo in the update tokio example --- examples/update_activity_tokio.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/update_activity_tokio.rs b/examples/update_activity_tokio.rs index 364bdfd..ebb2caf 100644 --- a/examples/update_activity_tokio.rs +++ b/examples/update_activity_tokio.rs @@ -173,5 +173,4 @@ async fn main() -> Result { #[cfg(not(feature = "tokio-runtime"))] fn main() { eprintln!("This example requires the `tokio-runtime` feature."); - eprintln!("Run with: cargo run --example async_tokio_reconnect --features tokio-runtime"); } From dfab29ce488cb28dd1d091d382134d4cdbcf51c4 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Wed, 15 Oct 2025 14:55:24 +0530 Subject: [PATCH 11/62] dev: remnaed utils to nonce --- examples/builder_all.rs | 3 ++- src/async_io/client.rs | 2 +- src/client.rs | 6 ++++-- src/ipc/connection.rs | 3 ++- src/lib.rs | 2 +- src/{utils.rs => nonce.rs} | 0 src/sync/client.rs | 3 ++- 7 files changed, 12 insertions(+), 7 deletions(-) rename src/{utils.rs => nonce.rs} (100%) diff --git a/examples/builder_all.rs b/examples/builder_all.rs index e94ac9d..f67932d 100644 --- a/examples/builder_all.rs +++ b/examples/builder_all.rs @@ -86,7 +86,7 @@ fn main() -> Result { // Party: Shows "X of Y" (e.g., "2 of 4" for a party) // Useful for multiplayer games showing current players // Parameters: party_id, current_size, max_size - .party("party-12345", 2, 4) + // .party("party-12345", 2, 4) // ═══════════════════════════════════════════════════════════ // BUTTONS (Clickable buttons - max 2) // ═══════════════════════════════════════════════════════════ @@ -118,6 +118,7 @@ fn main() -> Result { .build(); // Set the activity + // println!("{activity:?}"); client.set_activity(&activity)?; println!("✓ Activity set successfully!"); println!("\n📱 Check your Discord profile to see the activity!"); diff --git a/src/async_io/client.rs b/src/async_io/client.rs index 402d155..8217bbb 100644 --- a/src/async_io/client.rs +++ b/src/async_io/client.rs @@ -12,7 +12,7 @@ use crate::activity::Activity; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; use crate::ipc::{constants, Command, HandshakePayload, IpcMessage, Opcode}; -use crate::utils::generate_nonce; +use crate::nonce::generate_nonce; /// Async implementation of Discord IPC client pub struct AsyncDiscordIpcClient diff --git a/src/client.rs b/src/client.rs index 8f3be1b..7365615 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,5 +1,6 @@ use serde_json::{json, Value}; use std::collections::VecDeque; +use std::io::Write; use std::process; use std::time::{Duration, Instant}; @@ -9,7 +10,7 @@ use crate::error::{DiscordIpcError, Result}; use crate::ipc::{ constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, }; -use crate::utils::generate_nonce; +use crate::nonce::generate_nonce; /// Discord IPC Client pub struct DiscordIpcClient { @@ -194,8 +195,9 @@ impl DiscordIpcClient { }), nonce: nonce.clone(), }; - let payload = serde_json::to_value(message)?; + // debug_println!("[IPC_MESSAGE] : {:?} ", payload); + // std::io::stdout().flush().unwrap(); self.connection.send(Opcode::Frame, &payload)?; // Receive the response to check for errors diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 533a9d4..7f20e9f 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -11,6 +11,7 @@ use std::fs::OpenOptions; #[cfg(windows)] use std::io::{BufReader, BufWriter}; +use crate::debug_println; use crate::error::{DiscordIpcError, ProtocolContext, Result}; use crate::ipc::protocol::{constants, Opcode}; @@ -410,8 +411,8 @@ impl IpcConnection { /// Send data with opcode pub fn send(&mut self, opcode: Opcode, payload: &Value) -> Result<()> { let raw = serde_json::to_vec(payload)?; - // Clear and prepare write buffer + // debug_println!("[RAW] : {raw:?}"); self.write_buf.clear(); self.write_buf.reserve(8 + raw.len()); diff --git a/src/lib.rs b/src/lib.rs index 25b64be..5fbfee5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,8 +177,8 @@ pub mod client; pub mod error; pub mod ipc; pub mod macros; +pub mod nonce; pub mod retry; -pub mod utils; // Re-export the main public API #[cfg(feature = "secrets")] diff --git a/src/utils.rs b/src/nonce.rs similarity index 100% rename from src/utils.rs rename to src/nonce.rs diff --git a/src/sync/client.rs b/src/sync/client.rs index f995ac4..1773611 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -9,7 +9,7 @@ use crate::error::{DiscordIpcError, Result}; use crate::ipc::{ constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, }; -use crate::utils::generate_nonce; +use crate::nonce::generate_nonce; /// Discord IPC Client pub struct DiscordIpcClient { @@ -206,6 +206,7 @@ impl DiscordIpcClient { }; let payload = serde_json::to_value(message)?; + debug_println!("[PAYLOAD]: {:?} ", payload); self.connection.send(Opcode::Frame, &payload)?; // Receive the response to check for errors From 25567c84385daa3fa26af4b3976c47390345770a Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Wed, 15 Oct 2025 21:16:30 +0530 Subject: [PATCH 12/62] dev: fixed cargo docs --- src/lib.rs | 7 ++++--- src/nonce.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5fbfee5..d98cd06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ //! //! ## Synchronous Example //! -//! ```rust +//! ```rust no_run //! use presenceforge::{DiscordIpcClient, ActivityBuilder}; //! //! # fn main() -> Result<(), Box> { @@ -173,13 +173,14 @@ pub mod activity; pub mod async_io; -pub mod client; +// pub mod client; +pub mod core; pub mod error; pub mod ipc; pub mod macros; pub mod nonce; pub mod retry; - +pub use core as client; // Re-export the main public API #[cfg(feature = "secrets")] pub use activity::ActivitySecrets; diff --git a/src/nonce.rs b/src/nonce.rs index d76fc7c..b49562b 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -17,7 +17,7 @@ use uuid::Uuid; /// # Examples /// /// ``` -/// # use presenceforge::utils::generate_nonce; +/// # use presenceforge::nonce::generate_nonce; /// let nonce = generate_nonce("set-activity"); /// assert!(nonce.starts_with("set-activity-")); /// ``` From 07b23fcdd42e8f030094b70f81f018b71c49d231 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Wed, 15 Oct 2025 21:21:53 +0530 Subject: [PATCH 13/62] dev: removed redadunt implentation of client.rs --- src/client.rs | 441 -------------------------------------------------- src/lib.rs | 3 - 2 files changed, 444 deletions(-) delete mode 100644 src/client.rs diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 7365615..0000000 --- a/src/client.rs +++ /dev/null @@ -1,441 +0,0 @@ -use serde_json::{json, Value}; -use std::collections::VecDeque; -use std::io::Write; -use std::process; -use std::time::{Duration, Instant}; - -use crate::activity::Activity; -use crate::debug_println; -use crate::error::{DiscordIpcError, Result}; -use crate::ipc::{ - constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, -}; -use crate::nonce::generate_nonce; - -/// Discord IPC Client -pub struct DiscordIpcClient { - client_id: String, - connection: IpcConnection, - pending_messages: VecDeque, -} - -impl DiscordIpcClient { - /// Create a new Discord IPC client (uses auto-discovery) - pub fn new>(client_id: S) -> Result { - Self::new_with_config(client_id, None) - } - - /// Create a new Discord IPC client with pipe configuration - /// - /// # Arguments - /// - /// * `client_id` - The Discord application client ID - /// * `config` - Optional pipe configuration. If `None`, auto-discovery is used. - /// - /// # Examples - /// - /// ```no_run - /// use presenceforge::{DiscordIpcClient, PipeConfig}; - /// - /// // Auto-discovery (default) - /// let client = DiscordIpcClient::new_with_config("client_id", None)?; - /// - /// // Connect to custom path - /// let client = DiscordIpcClient::new_with_config( - /// "client_id", - /// Some(PipeConfig::CustomPath("/tmp/discord-ipc-0".to_string())) - /// )?; - /// # Ok::<(), presenceforge::DiscordIpcError>(()) - /// ``` - pub fn new_with_config>( - client_id: S, - config: Option, - ) -> Result { - let client_id = client_id.into(); - let connection = IpcConnection::new_with_config(config)?; - - Ok(Self { - client_id, - connection, - pending_messages: VecDeque::new(), - }) - } - - /// Create a new Discord IPC client with a connection timeout (uses auto-discovery) - /// - /// # Arguments - /// - /// * `client_id` - The Discord application client ID - /// * `timeout_ms` - Connection timeout in milliseconds - /// - /// # Returns - /// - /// A new Discord IPC client - /// - /// # Errors - /// - /// Returns a `DiscordIpcError::ConnectionTimeout` if the connection times out - pub fn new_with_timeout>(client_id: S, timeout_ms: u64) -> Result { - Self::new_with_config_and_timeout(client_id, None, timeout_ms) - } - - /// Create a new Discord IPC client with pipe configuration and timeout - /// - /// # Arguments - /// - /// * `client_id` - The Discord application client ID - /// * `config` - Optional pipe configuration. If `None`, auto-discovery is used. - /// * `timeout_ms` - Connection timeout in milliseconds - /// - /// # Examples - /// - /// ```no_run - /// use presenceforge::{DiscordIpcClient, PipeConfig}; - /// - /// // Auto-discovery with timeout - /// let client = DiscordIpcClient::new_with_config_and_timeout("client_id", None, 5000)?; - /// - /// // Custom pipe path with timeout - /// let client = DiscordIpcClient::new_with_config_and_timeout( - /// "client_id", - /// Some(PipeConfig::CustomPath("/tmp/discord-ipc-0".to_string())), - /// 5000 - /// )?; - /// # Ok::<(), presenceforge::DiscordIpcError>(()) - /// ``` - pub fn new_with_config_and_timeout>( - client_id: S, - config: Option, - timeout_ms: u64, - ) -> Result { - let client_id = client_id.into(); - let connection = IpcConnection::new_with_config_and_timeout(config, timeout_ms)?; - - Ok(Self { - client_id, - connection, - pending_messages: VecDeque::new(), - }) - } - - /// Perform handshake with Discord - /// - /// # Returns - /// - /// The Discord handshake response as a JSON Value - /// - /// # Errors - /// - /// Returns a `DiscordIpcError::HandshakeFailed` if the handshake fails - pub fn connect(&mut self) -> Result { - self.pending_messages.clear(); - - let handshake = HandshakePayload { - v: constants::IPC_VERSION, - client_id: self.client_id.clone(), - }; - - let payload = - serde_json::to_value(handshake).map_err(DiscordIpcError::SerializationFailed)?; - - self.connection.send(Opcode::Handshake, &payload)?; - - let (opcode, response) = self.connection.recv()?; - debug_println!("Handshake response: {}", response); - - // Check for error in the response - if let Some(err) = response.get("error") { - if let (Some(code), Some(message)) = ( - err.get("code").and_then(|c| c.as_i64()), - err.get("message").and_then(|m| m.as_str()), - ) { - return Err(DiscordIpcError::discord_error(code as i32, message)); - } else { - return Err(DiscordIpcError::HandshakeFailed(format!( - "Invalid error format: {}", - err - ))); - } - } - - // Verify opcode is correct for handshake response - if !opcode.is_handshake_response() { - return Err(DiscordIpcError::HandshakeFailed(format!( - "Expected handshake response opcode, got {:?}", - opcode - ))); - } - - Ok(response) - } - - /// Set Discord Rich Presence activity - /// - /// # Arguments - /// - /// * `activity` - The activity to set - /// - /// # Errors - /// - /// Returns a `DiscordIpcError` if serialization fails or if Discord returns an error - pub fn set_activity(&mut self, activity: &Activity) -> Result { - // Validate the activity first - if let Err(reason) = activity.validate() { - return Err(DiscordIpcError::InvalidActivity(reason)); - } - - // Generate a cryptographically secure unique nonce for this request - let nonce = generate_nonce("set-activity"); - - let message = IpcMessage { - cmd: Command::SetActivity, - args: json!({ - "pid": process::id(), - "activity": activity - }), - nonce: nonce.clone(), - }; - let payload = serde_json::to_value(message)?; - // debug_println!("[IPC_MESSAGE] : {:?} ", payload); - // std::io::stdout().flush().unwrap(); - self.connection.send(Opcode::Frame, &payload)?; - - // Receive the response to check for errors - let (opcode, response) = self.recv_for_nonce(&nonce)?; - - // Check if we got the correct response type - if !opcode.is_frame_response() { - return Err(DiscordIpcError::InvalidResponse(format!( - "Expected frame response, got {:?}", - opcode - ))); - } - - // Check for error in the response - if let Some(err) = response.get("error") { - if let (Some(code), Some(message)) = ( - err.get("code").and_then(|c| c.as_i64()), - err.get("message").and_then(|m| m.as_str()), - ) { - return Err(DiscordIpcError::discord_error(code as i32, message)); - } else { - return Err(DiscordIpcError::InvalidResponse(format!( - "Invalid error format in response: {}", - err - ))); - } - } - - // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } - } - - Ok(()) - } - - /// Clear Discord Rich Presence activity - /// - /// # Returns - /// - /// The response from Discord as a JSON Value - /// - /// # Errors - /// - /// Returns a `DiscordIpcError` if communication fails or if Discord returns an error - pub fn clear_activity(&mut self) -> Result { - // Generate a cryptographically secure unique nonce - let nonce = generate_nonce("clear-activity"); - - let message = IpcMessage { - cmd: Command::SetActivity, - args: json!({ - "pid": process::id(), - "activity": Value::Null - }), - nonce: nonce.clone(), - }; - - let payload = serde_json::to_value(message)?; - self.connection.send(Opcode::Frame, &payload)?; - - let (opcode, response) = self.recv_for_nonce(&nonce)?; - debug_println!("Clear Activity response: {}", response); - - // Check if we got the correct response type - if !opcode.is_frame_response() { - return Err(DiscordIpcError::InvalidResponse(format!( - "Expected frame response, got {:?}", - opcode - ))); - } - - // Check for error in the response - if let Some(err) = response.get("error") { - if let (Some(code), Some(message)) = ( - err.get("code").and_then(|c| c.as_i64()), - err.get("message").and_then(|m| m.as_str()), - ) { - return Err(DiscordIpcError::discord_error(code as i32, message)); - } else { - return Err(DiscordIpcError::InvalidResponse(format!( - "Invalid error format in response: {}", - err - ))); - } - } - - // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } - } - - Ok(response) - } - - /// Send a raw IPC message - pub fn send_message(&mut self, opcode: Opcode, payload: &Value) -> Result { - self.connection.send(opcode, payload) - } - - /// Receive a raw IPC message - pub fn recv_message(&mut self) -> Result<(Opcode, Value)> { - self.next_message() - } - - /// Remove pending responses older than the provided `max_age` and return how many were dropped. - pub fn cleanup_pending(&mut self, max_age: Duration) -> usize { - if max_age.is_zero() { - let dropped = self.pending_messages.len(); - self.pending_messages.clear(); - return dropped; - } - - let now = Instant::now(); - let original_len = self.pending_messages.len(); - self.pending_messages - .retain(|message| now.saturating_duration_since(message.received_at) <= max_age); - original_len - self.pending_messages.len() - } - - /// Close the connection - pub fn close(&mut self) { - self.connection.close(); - self.pending_messages.clear(); - } -} - -impl Drop for DiscordIpcClient { - fn drop(&mut self) { - self.close(); - } -} - -impl DiscordIpcClient { - fn next_message(&mut self) -> Result<(Opcode, Value)> { - if let Some(message) = self.pending_messages.pop_front() { - let PendingMessage { - opcode, payload, .. - } = message; - return Ok((opcode, payload)); - } - - self.connection.recv() - } - - fn recv_for_nonce(&mut self, expected_nonce: &str) -> Result<(Opcode, Value)> { - if let Some(message) = self.take_pending_by_nonce(expected_nonce) { - return Ok(message); - } - - loop { - let (opcode, response) = self.connection.recv()?; - if Self::value_has_nonce(&response, expected_nonce) { - return Ok((opcode, response)); - } - - self.pending_messages - .push_back(PendingMessage::new(opcode, response)); - } - } - - fn take_pending_by_nonce(&mut self, expected_nonce: &str) -> Option<(Opcode, Value)> { - let position = self - .pending_messages - .iter() - .position(|message| Self::value_has_nonce(&message.payload, expected_nonce)); - - position.and_then(|index| { - self.pending_messages.remove(index).map(|message| { - let PendingMessage { - opcode, payload, .. - } = message; - (opcode, payload) - }) - }) - } - - fn value_has_nonce(value: &Value, expected_nonce: &str) -> bool { - value - .get("nonce") - .and_then(|n| n.as_str()) - .map(|actual| actual == expected_nonce) - .unwrap_or(false) - } -} - -#[derive(Debug)] -struct PendingMessage { - opcode: Opcode, - payload: Value, - received_at: Instant, -} - -impl PendingMessage { - fn new(opcode: Opcode, payload: Value) -> Self { - Self { - opcode, - payload, - received_at: Instant::now(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn value_has_nonce_detects_match() { - let payload = serde_json::json!({ - "nonce": "abc", - "data": {} - }); - - assert!(DiscordIpcClient::value_has_nonce(&payload, "abc")); - assert!(!DiscordIpcClient::value_has_nonce(&payload, "def")); - } - - #[test] - fn value_has_nonce_handles_missing_field() { - let payload = serde_json::json!({ "data": 1 }); - assert!(!DiscordIpcClient::value_has_nonce(&payload, "anything")); - } - - #[test] - fn pending_message_records_creation_time() { - let message = PendingMessage::new(Opcode::Frame, serde_json::json!({"nonce": "1"})); - let elapsed = Instant::now().saturating_duration_since(message.received_at); - assert!(elapsed.as_secs() < 1); - } -} diff --git a/src/lib.rs b/src/lib.rs index d98cd06..0597659 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,14 +173,11 @@ pub mod activity; pub mod async_io; -// pub mod client; -pub mod core; pub mod error; pub mod ipc; pub mod macros; pub mod nonce; pub mod retry; -pub use core as client; // Re-export the main public API #[cfg(feature = "secrets")] pub use activity::ActivitySecrets; From c28d256f482694abb1b1ed3e01b2be453bc945fd Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Wed, 15 Oct 2025 21:26:45 +0530 Subject: [PATCH 14/62] Update import path for DiscordIpcClient in API reference --- docs/API_REFERENCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 0b38f8c..a5a7719 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -27,7 +27,7 @@ The synchronous Discord IPC client for managing Rich Presence. Creates a new Discord IPC client with automatic pipe discovery. ```rust -use presenceforge::DiscordIpcClient; +use presenceforge::sync::DiscordIpcClient; let client = DiscordIpcClient::new("your_client_id")?; ``` From b96d17b56668d3061a7856b086909b6f75f27db6 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Wed, 15 Oct 2025 23:52:26 +0530 Subject: [PATCH 15/62] dev: fixed api changes and updated the code base to use the new explicity sync based api --- README.md | 12 ++++++------ docs/ACTIVITY_BUILDER_REFERENCE.md | 3 ++- docs/API_REFERENCE.md | 3 ++- docs/ASYNC_RUNTIMES.md | 3 ++- docs/ERROR_HANDLING.md | 26 +++++++++++++++++--------- docs/FAQ.md | 3 ++- docs/GETTING_STARTED.md | 3 ++- docs/PIPE_SELECTION.md | 3 ++- examples/basic.rs | 3 ++- examples/basic_flatpak.rs | 3 ++- examples/builder_all.rs | 3 ++- examples/coding_status.rs | 3 ++- examples/connection_retry.rs | 3 ++- examples/game_demo.rs | 4 ++-- examples/pipe_selection.rs | 4 ++-- src/error.rs | 7 ++++--- src/lib.rs | 4 ++-- src/retry.rs | 4 ++-- src/sync/client.rs | 10 +++++----- 19 files changed, 62 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index db4782b..b7b4b2a 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features ### Basic Usage (Synchronous) ```rust -use presenceforge::{DiscordIpcClient, ActivityBuilder}; - +use presenceforge::ActivityBuilder; +use presenceforge::sync::DiscordIpcClient fn main() -> Result<(), Box> { let mut client = DiscordIpcClient::new("your_client_id")?; client.connect()?; @@ -173,8 +173,8 @@ fn main() -> Result { ### Game Integration ```rust -use presenceforge::{ActivityBuilder, DiscordIpcClient}; - +use presenceforge::ActivityBuilder; +use presenceforge::sync::DiscordIpcClient; let activity = ActivityBuilder::new() .state("Forest Level") .details("Fighting goblins") @@ -341,8 +341,8 @@ cargo run --example async_tokio --features tokio-runtime PresenceForge uses the `Result` type for error handling: ```rust -use presenceforge::{DiscordIpcClient, DiscordIpcError}; - +use presenceforge::DiscordIpcError; +use presenceforge::sync::DiscordIpcClient; match client.connect() { Ok(_) => println!("Connected successfully!"), Err(DiscordIpcError::ConnectionFailed) => { diff --git a/docs/ACTIVITY_BUILDER_REFERENCE.md b/docs/ACTIVITY_BUILDER_REFERENCE.md index bcb7766..cd7ae26 100644 --- a/docs/ACTIVITY_BUILDER_REFERENCE.md +++ b/docs/ACTIVITY_BUILDER_REFERENCE.md @@ -238,7 +238,8 @@ let end_time = now + 300; Here's an activity using all fields: ```rust -use presenceforge::{ActivityBuilder, DiscordIpcClient}; +use presenceforge::DiscordIpcClient; +use presenceforge::sync::DiscordIpcClient; let activity = ActivityBuilder::new() // Text diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index a5a7719..be523d3 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -49,7 +49,8 @@ let client = DiscordIpcClient::new("your_client_id")?; Creates a new Discord IPC client with custom pipe configuration. ```rust -use presenceforge::{DiscordIpcClient, PipeConfig}; +use presenceforge::PipeConfig; +use presenceforge::sync::DiscordIpcClient; // Auto-discovery (equivalent to ::new()) let client = DiscordIpcClient::new_with_config("client_id", None)?; diff --git a/docs/ASYNC_RUNTIMES.md b/docs/ASYNC_RUNTIMES.md index f9eba25..2e2ab3c 100644 --- a/docs/ASYNC_RUNTIMES.md +++ b/docs/ASYNC_RUNTIMES.md @@ -639,7 +639,8 @@ async fn main() -> Result { ### Sync Code ```rust -use presenceforge::{DiscordIpcClient, ActivityBuilder}; +use presenceforge::ActivityBuilder; +use presenceforge::sync::DiscordIpcClient; fn main() -> Result<(), Box> { let mut client = DiscordIpcClient::new("client_id")?; diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index 965110d..c89ec6e 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -23,7 +23,8 @@ PresenceForge uses the `DiscordIpcError` enum for all error cases. All fallible ### Basic Error Handling ```rust -use presenceforge::{DiscordIpcClient, Result}; +use presenceforge::Result; +use presenceforge::sync::DiscordIpcClient; fn main() -> Result<(), Box> { let mut client = DiscordIpcClient::new("your_client_id")?; @@ -51,7 +52,8 @@ Common causes: - Permission denied accessing pipe/socket ```rust -use presenceforge::{DiscordIpcClient, DiscordIpcError}; +use presenceforge::DiscordIpcError; +use presenceforge::sync::DiscordIpcClient; match DiscordIpcClient::new("client_id") { Ok(mut client) => { @@ -202,7 +204,8 @@ if error.is_recoverable() { **Problem:** Connection fails because Discord isn't running. ```rust -use presenceforge::{DiscordIpcClient, DiscordIpcError}; +use presenceforge:: DiscordIpcError; +use presenceforge::sync::DiscordIpcClient; fn connect_to_discord(client_id: &str) -> Result> { match DiscordIpcClient::new(client_id) { @@ -228,7 +231,8 @@ fn connect_to_discord(client_id: &str) -> Result Result<(), Box> { @@ -306,7 +311,7 @@ For initial connection, use the `with_retry` function with automatic exponential ```rust use presenceforge::retry::{with_retry, RetryConfig}; -use presenceforge::DiscordIpcClient; +use presenceforge::sync::DiscordIpcClient; fn main() -> Result<(), Box> { // Default: 3 attempts, 1s initial delay, exponential backoff @@ -432,7 +437,8 @@ fn main() -> Result<(), Box> { **Problem:** Environment or configuration issues prevent connection. ```rust -use presenceforge::{DiscordIpcClient, DiscordIpcError}; +use presenceforge::DiscordIpcError; +use presenceforge::sync::DiscordIpcClient; use std::env; fn setup_client() -> Result> { @@ -551,7 +557,8 @@ PresenceForge includes built-in retry utilities with exponential backoff: ```rust use presenceforge::retry::{with_retry, RetryConfig}; -use presenceforge::DiscordIpcClient; + +use presenceforge::sync::DiscordIpcClient; fn connect_with_retry(client_id: &str) -> Result> { // Use default retry config (3 attempts, 1s initial delay, exponential backoff) @@ -591,7 +598,8 @@ let mut client = with_retry(&config, || { ### 5. Clean Up on Errors ```rust -use presenceforge::{DiscordIpcClient, ActivityBuilder}; +use presenceforge::ActivityBuilder; +use presenceforge::sync::DiscordIpcClient; fn run_presence() -> Result<(), Box> { let mut client = DiscordIpcClient::new("client_id")?; diff --git a/docs/FAQ.md b/docs/FAQ.md index b516656..5c5da2e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -160,7 +160,8 @@ Error: ProtocolError("Handshake failed") 4. **Try a different pipe:** ```rust - use presenceforge::{IpcConnection, PipeConfig, DiscordIpcClient}; + use presenceforge::{IpcConnection, PipeConfig}; + use presenceforge:: DiscordIpcClient; let pipes = IpcConnection::discover_pipes(); for pipe in pipes { diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 2d0709d..0f95b43 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -79,7 +79,8 @@ presenceforge = { git = "https://github.com/Sreehari425/presenceforge" } Edit `src/main.rs`: ```rust -use presenceforge::{DiscordIpcClient, ActivityBuilder}; +use presenceforge::DiscordIpcClient; +use presenceforge::sync::DiscordIpcClient; use std::thread; use std::time::Duration; diff --git a/docs/PIPE_SELECTION.md b/docs/PIPE_SELECTION.md index 864875a..1ea8b1f 100644 --- a/docs/PIPE_SELECTION.md +++ b/docs/PIPE_SELECTION.md @@ -300,8 +300,9 @@ let client = DiscordIpcClient::new_with_config( Connection errors are handled through the standard `DiscordIpcError` enum: ```rust -use presenceforge::{DiscordIpcError, PipeConfig, DiscordIpcClient}; +use presenceforge::{DiscordIpcError, PipeConfig}; +use presenceforge::sync::DiscordIpcClient; match DiscordIpcClient::new_with_config( "client_id", Some(PipeConfig::CustomPath("/invalid/path".to_string())) diff --git a/examples/basic.rs b/examples/basic.rs index cfb1f92..9d35f3a 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,6 @@ use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, Result}; use std::time::Duration; /// Discord Rich Presence Basic Example diff --git a/examples/basic_flatpak.rs b/examples/basic_flatpak.rs index 0041a12..61af550 100644 --- a/examples/basic_flatpak.rs +++ b/examples/basic_flatpak.rs @@ -5,7 +5,8 @@ // If Flatpak Discord is not active, it will fallback to standard Discord. use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, IpcConnection, PipeConfig, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, IpcConnection, PipeConfig, Result}; use std::time::Duration; /// Discord Rich Presence Flatpak Example diff --git a/examples/builder_all.rs b/examples/builder_all.rs index f67932d..a327473 100644 --- a/examples/builder_all.rs +++ b/examples/builder_all.rs @@ -4,7 +4,8 @@ // of what each field does and how it appears in Discord. use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, Result}; use std::time::Duration; /// Discord Rich Presence Complete Builder Example diff --git a/examples/coding_status.rs b/examples/coding_status.rs index 7f8a8dc..3e57e44 100644 --- a/examples/coding_status.rs +++ b/examples/coding_status.rs @@ -1,5 +1,6 @@ use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, Result}; use std::time::Duration; /// Discord Rich Presence Coding Status Example diff --git a/examples/connection_retry.rs b/examples/connection_retry.rs index 25058ed..9cd438b 100644 --- a/examples/connection_retry.rs +++ b/examples/connection_retry.rs @@ -1,6 +1,7 @@ use clap::Parser; use presenceforge::retry::{with_retry, RetryConfig}; -use presenceforge::{ActivityBuilder, DiscordIpcClient, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, Result}; use std::time::Duration; /// Discord Rich Presence Connection Retry Example diff --git a/examples/game_demo.rs b/examples/game_demo.rs index e25d091..c1dc759 100644 --- a/examples/game_demo.rs +++ b/examples/game_demo.rs @@ -1,8 +1,8 @@ use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, Result}; +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, Result}; use std::thread; use std::time::Duration; - /// Discord Rich Presence Game Demo Example #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] diff --git a/examples/pipe_selection.rs b/examples/pipe_selection.rs index f090086..0f74ce2 100644 --- a/examples/pipe_selection.rs +++ b/examples/pipe_selection.rs @@ -1,8 +1,8 @@ // Example demonstrating pipe selection and discovery features use clap::Parser; -use presenceforge::{ActivityBuilder, DiscordIpcClient, IpcConnection, PipeConfig}; - +use presenceforge::sync::DiscordIpcClient; +use presenceforge::{ActivityBuilder, IpcConnection, PipeConfig}; /// Discord IPC Pipe Selection Example #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] diff --git a/src/error.rs b/src/error.rs index a89ac85..dc86180 100644 --- a/src/error.rs +++ b/src/error.rs @@ -77,8 +77,8 @@ impl Display for ErrorCategory { /// /// Basic error handling: /// ```rust -/// use presenceforge::{DiscordIpcClient, DiscordIpcError}; -/// +/// use presenceforge::DiscordIpcError; +/// use presenceforge::sync::DiscordIpcClient; /// fn main() -> Result<(), Box> { /// let mut client = match DiscordIpcClient::new("your-client-id") { /// Ok(client) => client, @@ -97,7 +97,8 @@ impl Display for ErrorCategory { /// /// Using utility functions for recoverable errors: /// ```rust -/// use presenceforge::{DiscordIpcClient, DiscordIpcError, Activity}; +/// use presenceforge::{DiscordIpcError, Activity}; +/// use presenceforge::sync::DiscordIpcClient; /// use std::time::Duration; /// /// fn connect_with_retry(client_id: &str, max_attempts: u32) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 0597659..371174b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,8 +16,8 @@ //! ## Synchronous Example //! //! ```rust no_run -//! use presenceforge::{DiscordIpcClient, ActivityBuilder}; -//! +//! use presenceforge::ActivityBuilder; +//! use presenceforge::sync::DiscordIpcClient; //! # fn main() -> Result<(), Box> { //! let mut client = DiscordIpcClient::new("your_client_id")?; //! client.connect()?; diff --git a/src/retry.rs b/src/retry.rs index 0559fdd..4fced43 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -79,8 +79,8 @@ impl RetryConfig { /// # Example /// /// ```no_run -/// use presenceforge::{DiscordIpcClient, retry::{with_retry, RetryConfig}}; -/// +/// use presenceforge::retry::{with_retry, RetryConfig}; +/// use presenceforge::sync::DiscordIpcClient; /// let config = RetryConfig::with_max_attempts(5); /// /// let client = with_retry(&config, || { diff --git a/src/sync/client.rs b/src/sync/client.rs index 1773611..01ec0ca 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -36,8 +36,8 @@ impl DiscordIpcClient { /// # Examples /// /// ```no_run - /// use presenceforge::{DiscordIpcClient, PipeConfig}; - /// + /// use presenceforge::PipeConfig; + /// use presenceforge::sync::DiscordIpcClient; /// // Auto-discovery (default) /// let client = DiscordIpcClient::new_with_config("client_id", None)?; /// @@ -97,8 +97,8 @@ impl DiscordIpcClient { /// # Examples /// /// ```no_run - /// use presenceforge::{DiscordIpcClient, PipeConfig}; - /// + /// use presenceforge::PipeConfig; + /// use presenceforge::sync::DiscordIpcClient; /// // Auto-discovery with timeout /// let client = DiscordIpcClient::new_with_config_and_timeout("client_id", None, 5000)?; /// @@ -360,7 +360,7 @@ impl DiscordIpcClient { /// # Examples /// /// ```no_run - /// use presenceforge::DiscordIpcClient; + /// use presenceforge::sync::DiscordIpcClient; /// use presenceforge::ActivityBuilder; /// /// let mut client = DiscordIpcClient::new("client_id")?; From 78bfe616d8096d1780b16697188befc24faab81c Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Thu, 16 Oct 2025 15:24:27 +0530 Subject: [PATCH 16/62] dev: applied clippy suggetion --- src/async_io/async_std/mod.rs | 19 +++++++------------ src/async_io/tokio/mod.rs | 19 +++++++------------ src/ipc/connection.rs | 1 - src/retry.rs | 15 +++------------ 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/src/async_io/async_std/mod.rs b/src/async_io/async_std/mod.rs index 5479fab..f252873 100644 --- a/src/async_io/async_std/mod.rs +++ b/src/async_io/async_std/mod.rs @@ -487,18 +487,13 @@ pub mod client { } impl AsyncStdClientExt for AsyncDiscordIpcClient { - fn connect_with_timeout( - &mut self, - timeout_duration: Duration, - ) -> impl std::future::Future> + Send { - async move { - match async_std::future::timeout(timeout_duration, self.connect()).await { - Ok(result) => result, - Err(_) => Err(DiscordIpcError::connection_timeout( - timeout_duration.as_millis() as u64, - None, - )), - } + async fn connect_with_timeout(&mut self, timeout_duration: Duration) -> Result { + match async_std::future::timeout(timeout_duration, self.connect()).await { + Ok(result) => result, + Err(_) => Err(DiscordIpcError::connection_timeout( + timeout_duration.as_millis() as u64, + None, + )), } } } diff --git a/src/async_io/tokio/mod.rs b/src/async_io/tokio/mod.rs index 82972af..7864cd5 100644 --- a/src/async_io/tokio/mod.rs +++ b/src/async_io/tokio/mod.rs @@ -423,18 +423,13 @@ pub mod client { } impl TokioClientExt for AsyncDiscordIpcClient { - fn connect_with_timeout( - &mut self, - timeout_duration: Duration, - ) -> impl std::future::Future> + Send { - async move { - match timeout(timeout_duration, self.connect()).await { - Ok(result) => result, - Err(_) => Err(DiscordIpcError::connection_timeout( - timeout_duration.as_millis() as u64, - None, - )), - } + async fn connect_with_timeout(&mut self, timeout_duration: Duration) -> Result { + match timeout(timeout_duration, self.connect()).await { + Ok(result) => result, + Err(_) => Err(DiscordIpcError::connection_timeout( + timeout_duration.as_millis() as u64, + None, + )), } } } diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 7f20e9f..9b6692e 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -11,7 +11,6 @@ use std::fs::OpenOptions; #[cfg(windows)] use std::io::{BufReader, BufWriter}; -use crate::debug_println; use crate::error::{DiscordIpcError, ProtocolContext, Result}; use crate::ipc::protocol::{constants, Opcode}; diff --git a/src/retry.rs b/src/retry.rs index 4fced43..9ded736 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -167,10 +167,7 @@ where } Err(last_error.unwrap_or_else(|| { - DiscordIpcError::ConnectionFailed(std::io::Error::new( - std::io::ErrorKind::Other, - "Retry attempts exhausted", - )) + DiscordIpcError::ConnectionFailed(std::io::Error::other("Retry attempts exhausted")) })) } @@ -226,10 +223,7 @@ where } Err(last_error.unwrap_or_else(|| { - DiscordIpcError::ConnectionFailed(std::io::Error::new( - std::io::ErrorKind::Other, - "Retry attempts exhausted", - )) + DiscordIpcError::ConnectionFailed(std::io::Error::other("Retry attempts exhausted")) })) } @@ -286,10 +280,7 @@ where } Err(last_error.unwrap_or_else(|| { - DiscordIpcError::ConnectionFailed(std::io::Error::new( - std::io::ErrorKind::Other, - "Retry attempts exhausted", - )) + DiscordIpcError::ConnectionFailed(std::io::Error::other("Retry attempts exhausted")) })) } From b55be4e7ffe4ea9a1f806c7185f64bac6c4edb02 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Thu, 16 Oct 2025 15:31:45 +0530 Subject: [PATCH 17/62] dev: improved macros debug_println --- src/macros.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/macros.rs b/src/macros.rs index 470b17e..f3748ce 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,12 +1,16 @@ /// Global debug flag to control debug output /// Set to `true` to enable debug prints or use PRESENCEFORGE_DEBUG=1 environment variable -#[doc(hidden)] +use std::sync::OnceLock; + +static DEBUG_ENABLED: OnceLock = OnceLock::new(); + pub fn is_debug_enabled() -> bool { - std::env::var("PRESENCEFORGE_DEBUG") - .map(|val| val == "1") - .unwrap_or(false) + *DEBUG_ENABLED.get_or_init(|| { + std::env::var("PRESENCEFORGE_DEBUG") + .map(|val| val == "1" || val.to_lowercase() == "true") + .unwrap_or(false) + }) } - /// Macro for conditional debug printing #[macro_export] macro_rules! debug_println { From 049fcbcb67d4d94c565859a085828198baf027ed Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Thu, 16 Oct 2025 15:42:51 +0530 Subject: [PATCH 18/62] doc: updated genrate_nonce description --- src/nonce.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nonce.rs b/src/nonce.rs index b49562b..8b0f3dc 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -25,8 +25,7 @@ use uuid::Uuid; /// # Security /// /// UUID v4 provides 122 bits of randomness, making collisions extremely unlikely -/// (probability of collision is approximately 1 in 2^61 after generating 1 billion UUIDs). -/// This is far superior to timestamp-based nonces which can collide during rapid operations. +/// probability of collision is approximately 1 in 2^61 after generating 1 billion UUID :) pub fn generate_nonce(prefix: &str) -> String { format!("{}-{}", prefix, Uuid::new_v4()) } From 38378f3accd12156d62d48f01be260605bf8b689 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Thu, 16 Oct 2025 21:16:15 +0530 Subject: [PATCH 19/62] scripts: added clippy pedantic --- scripts/clippy_god_mode.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 scripts/clippy_god_mode.sh diff --git a/scripts/clippy_god_mode.sh b/scripts/clippy_god_mode.sh new file mode 100755 index 0000000..5d1a4a8 --- /dev/null +++ b/scripts/clippy_god_mode.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd .. # assuming you are runing from scripts directory +cargo clippy --all-targets --all-features -- -W clippy::pedantic -W clippy::nursery From dc556058a379f203080fe35f6545c23d019165af Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Fri, 17 Oct 2025 22:14:02 +0530 Subject: [PATCH 20/62] ci : removed ci to run on every pr --- .github/workflows/basic-ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/basic-ci.yml b/.github/workflows/basic-ci.yml index 40b2550..71ada76 100644 --- a/.github/workflows/basic-ci.yml +++ b/.github/workflows/basic-ci.yml @@ -4,9 +4,6 @@ on: push: branches: - main - pull_request: - branches: - - main workflow_dispatch: permissions: From 3eaa12883fc82e5cd5b5073e52735f7d68d8472a Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Fri, 17 Oct 2025 23:31:29 +0530 Subject: [PATCH 21/62] docs: updated docs (mostly fixed api which changed ) --- docs/ACTIVITY_BUILDER_REFERENCE.md | 12 ++++++------ docs/API_REFERENCE.md | 21 ++++++--------------- docs/ASYNC_RUNTIMES.md | 8 ++++---- docs/ERROR_HANDLING.md | 7 +++---- docs/FAQ.md | 3 +-- docs/GETTING_STARTED.md | 8 ++++---- 6 files changed, 24 insertions(+), 35 deletions(-) diff --git a/docs/ACTIVITY_BUILDER_REFERENCE.md b/docs/ACTIVITY_BUILDER_REFERENCE.md index cd7ae26..f014454 100644 --- a/docs/ACTIVITY_BUILDER_REFERENCE.md +++ b/docs/ACTIVITY_BUILDER_REFERENCE.md @@ -95,7 +95,7 @@ S = Small Image (overlays large image in bottom-right corner) #### Start Timestamp (Elapsed Time) ```rust -.start_timestamp_now().expect("timestamp") // Start counting from now +.start_timestamp_now()? // Start counting from now (returns Result) // or .start_timestamp(1234567890) // Unix timestamp (u64) ``` @@ -134,15 +134,15 @@ let end_time = now + 300; ### 6. **Party** (`party()`) -#### Note: Partialy tested feature +#### Note: Partially tested feature ```rust -.party("unique-party-id", 2, 4) // party_id, current_size, max_size +.party("unique-party-id", 2, 4) // id, current_size, max_size ``` - **Appears as:** "2 of 4" below the state text - **Parameters:** - - `party_id`: Unique identifier for the party (string) + - `id`: Unique identifier for the party (string) - `current_size`: Current number of players (u32) - `max_size`: Maximum number of players (u32) - **Use for:** Multiplayer games, voice channels, collaborative work @@ -238,8 +238,8 @@ let end_time = now + 300; Here's an activity using all fields: ```rust -use presenceforge::DiscordIpcClient; use presenceforge::sync::DiscordIpcClient; +use presenceforge::ActivityBuilder; let activity = ActivityBuilder::new() // Text @@ -253,7 +253,7 @@ let activity = ActivityBuilder::new() .small_text("Level 42 Warrior") // Time - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? // Party .party("party-12345", 3, 4) diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index be523d3..e14c7cc 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -311,28 +311,19 @@ Adds a button to the Rich Presence (max 2 buttons). ### Party Methods -#### `party_id(self, id: impl Into) -> Self` +#### `party(self, id: impl Into, current_size: u32, max_size: u32) -> Self` -Sets the party ID (for grouping players). +Sets the party information (ID and size) in a single method. ```rust -.party_id("party_12345") -``` - ---- - -#### `party_size(self, current: i32, max: i32) -> Self` - -Sets the party size display. - -```rust -.party_size(2, 4) // Shows "2 of 4" +.party("party_12345", 2, 4) // Shows "2 of 4" ``` **Parameters:** -- `current` - Current number of players -- `max` - Maximum number of players +- `id` - Unique party identifier for grouping players +- `current_size` - Current number of players +- `max_size` - Maximum number of players --- diff --git a/docs/ASYNC_RUNTIMES.md b/docs/ASYNC_RUNTIMES.md index 2e2ab3c..a995276 100644 --- a/docs/ASYNC_RUNTIMES.md +++ b/docs/ASYNC_RUNTIMES.md @@ -89,7 +89,7 @@ async fn setup_presence() -> Result { let activity = ActivityBuilder::new() .state("Playing async") .details("Runtime-agnostic!") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); // Set activity @@ -162,7 +162,7 @@ async fn main() -> Result { let activity = ActivityBuilder::new() .state("Playing async") .details("Using Tokio") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); // Set activity @@ -211,7 +211,7 @@ async fn main() -> Result { let activity = ActivityBuilder::new() .state(format!("Update #{}", counter)) .details("Tokio Background Task") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); let mut client = presence_client.lock().await; @@ -441,7 +441,7 @@ fn main() -> Result { let activity = ActivityBuilder::new() .state("Playing async") .details("Using smol") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); client.set_activity(&activity).await?; diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index c89ec6e..fd6361f 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -47,6 +47,7 @@ Below are the most common variants in `DiscordIpcError` and when they occur. See When the library fails to open/connect the IPC socket/pipe. Common causes: + - Discord is not running - No available IPC pipes/sockets - Permission denied accessing pipe/socket @@ -239,7 +240,7 @@ use std::time::Duration; fn maintain_presence(mut client: DiscordIpcClient) -> Result<(), Box> { let activity = ActivityBuilder::new() .state("Running") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); loop { @@ -591,8 +592,6 @@ let mut client = with_retry(&config, || { })?; ``` - - --- ### 5. Clean Up on Errors @@ -621,7 +620,7 @@ fn run_presence() -> Result<(), Box> { result } -```` +``` --- diff --git a/docs/FAQ.md b/docs/FAQ.md index 5c5da2e..703c10e 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -462,8 +462,7 @@ Yes! Use party methods: ```rust let activity = ActivityBuilder::new() .state("In a Party") - .party_size(2, 4) // 2 of 4 players - .party_id("party123") // Unique party ID + .party("party123", 2, 4) // party_id, current (2 of 4 players), max .build(); ``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 0f95b43..14e8e4a 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -79,7 +79,7 @@ presenceforge = { git = "https://github.com/Sreehari425/presenceforge" } Edit `src/main.rs`: ```rust -use presenceforge::DiscordIpcClient; +use presenceforge::ActivityBuilder; use presenceforge::sync::DiscordIpcClient; use std::thread; use std::time::Duration; @@ -98,7 +98,7 @@ fn main() -> Result<(), Box> { let activity = ActivityBuilder::new() .state("Hello, Discord!") .details("Using PresenceForge") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build(); // Set the activity @@ -216,7 +216,7 @@ thread::sleep(Duration::from_secs(5)); client.set_activity(&ActivityBuilder::new() .state("In Match") .details("Competitive Mode") - .start_timestamp_now().expect("timestamp") + .start_timestamp_now()? .build())?; ``` @@ -247,7 +247,7 @@ fn main() { } fn run() -> Result<(), Box> { - let mut client = DiscordIpcClient::new("your_client_id")?; + let mut client = presenceforge::sync::DiscordIpcClient::new("your_client_id")?; client.connect()?; let activity = ActivityBuilder::new() From 0fe2fabd9802eb45156c9645ae0461f54b40fdce Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 14:18:04 +0530 Subject: [PATCH 22/62] dev: added 'tmp' to env keys --- src/ipc/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 9b6692e..047cffc 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -100,7 +100,7 @@ impl IpcConnection { /// - `/run/user/{UID}` #[cfg(unix)] fn candidate_ipc_dir() -> Vec { - let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"]; + let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP", "tmp"]; let mut directories = Vec::new(); for key in &env_keys { if let Ok(dir) = std::env::var(key) { From 0a1cda430bbe3e330257f00882e097cd7d808de9 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 14:50:48 +0530 Subject: [PATCH 23/62] chore: bump to v0.1.0-dev and update docs notes --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 10 +++++----- docs/API_REFERENCE.md | 3 ++- docs/ASYNC_RUNTIMES.md | 3 ++- docs/ERROR_HANDLING.md | 1 - docs/FAQ.md | 1 - docs/GETTING_STARTED.md | 3 ++- 8 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98ce708..6d05d3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,7 +728,7 @@ dependencies = [ [[package]] name = "presenceforge" -version = "0.0.0" +version = "0.1.0-dev" dependencies = [ "async-std", "blocking", diff --git a/Cargo.toml b/Cargo.toml index d02e51e..c9f9e30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "presenceforge" -version = "0.0.0" +version = "0.1.0-dev" edition = "2021" authors = ["Sreehari Anil "] description = "A library for Discord Rich Presence (IPC) integration" diff --git a/README.md b/README.md index b7b4b2a..c7d9e82 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ A Rust library for Discord Rich Presence that actually works without the headach [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](https://github.com/Sreehari425/presenceforge#license) [![Rust](https://img.shields.io/badge/rust-1.70+-blue.svg)](https://www.rust-lang.org) -> **Note**: This is currently in early development (v0.0.0). Things might break +> **Note**: This is currently in development (v0.1.0-dev). Things might break > This is a learning/hobby project. -> -> ⚠️ FINAL WARNING: PresenceForge is a learning/hobby project. If you need production-ready Discord Rich Presence, use a mature library like pypresence, discord-rpc, or CraftPresence. +> Features and APIs may change in future versions. + ## Documentation @@ -26,7 +26,7 @@ A Rust library for Discord Rich Presence that actually works without the headach ## What Works - [x] Linux and macOS (Unix domain sockets) -- [x] Windows support (named pipes) - needs testing +- [x] Windows support (named pipes) - [x] Flatpak Discord support (automatic detection) - [x] Basic Rich Presence activities - [x] Activity builder pattern @@ -276,7 +276,7 @@ ActivityBuilder::new() .small_image("image_key") // Small image asset .small_text("Hover text") // Small image hover text .button("Label", "https://url") // Clickable button (max 2) - .party_size(1, 4) // Party size (current, max) + .party("id",1, 4) // Party size (current, max) .build() ``` diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index e14c7cc..9b21881 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -2,7 +2,8 @@ API reference for PresenceForge (work-in-progress; APIs may change). -> ⚠️ **NOTE:** This feature is experimental/untested. Use at your own risk. +> **Note:** PresenceForge v0.1.0-dev is an early development release. +> It’s functional, but features may change or be incomplete. ## Table of Contents diff --git a/docs/ASYNC_RUNTIMES.md b/docs/ASYNC_RUNTIMES.md index a995276..3313f61 100644 --- a/docs/ASYNC_RUNTIMES.md +++ b/docs/ASYNC_RUNTIMES.md @@ -1,6 +1,7 @@ # Async Runtimes Guide -> ⚠️ **NOTE:** This feature is experimental/untested. Use at your own risk. +> **Note:** PresenceForge v0.1.0-dev is an early development release. +> It’s functional, but features may change or be incomplete. ## Table of Contents diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index fd6361f..9a9af52 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -2,7 +2,6 @@ A comprehensive guide to handling errors and implementing retry logic in PresenceForge. -> ⚠️ **NOTE:** This feature is experimental/untested. Use at your own risk. ## Table of Contents diff --git a/docs/FAQ.md b/docs/FAQ.md index 703c10e..72fcc4d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -2,7 +2,6 @@ Frequently asked questions and solutions to common problems. -> ⚠️ **WARNING:** PresenceForge is an experimental, hobby project (v0.0.0). Features are partially tested, may break, and should **not** be used in production. ## Table of Contents diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 14e8e4a..5f4f4ec 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -2,7 +2,8 @@ Welcome to PresenceForge! This guide will help you get started with integrating Discord Rich Presence into your Rust application. -> ⚠️ **WARNING:** PresenceForge is an experimental, hobby project (v0.0.0). Features are partially tested, may break, and should **not** be used in production. +> **Note:** PresenceForge v0.1.0-dev is an early development release. +> It’s functional, but features may change or be incomplete. ## What is Discord Rich Presence? From cc3a293fec0c306f7f4a0a59011f7a309e6f8da5 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 14:58:10 +0530 Subject: [PATCH 24/62] chore: bump the edition to 2024 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c9f9e30..122f165 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "presenceforge" version = "0.1.0-dev" -edition = "2021" +edition = "2024" authors = ["Sreehari Anil "] description = "A library for Discord Rich Presence (IPC) integration" readme = "README.md" From 5b1e023d2df23490e43023f1889005fc57a753ec Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:06:24 +0530 Subject: [PATCH 25/62] chore: updated the changelogs --- README.md | 5 ++--- changelogs/{unreleased.md => 0.1.0-dev.md} | 0 2 files changed, 2 insertions(+), 3 deletions(-) rename changelogs/{unreleased.md => 0.1.0-dev.md} (100%) diff --git a/README.md b/README.md index c7d9e82..1db4e2f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A Rust library for Discord Rich Presence that actually works without the headach [![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](https://github.com/Sreehari425/presenceforge#license) [![Rust](https://img.shields.io/badge/rust-1.70+-blue.svg)](https://www.rust-lang.org) +![Crates.io Version](https://img.shields.io/crates/v/presenceforge) > **Note**: This is currently in development (v0.1.0-dev). Things might break > This is a learning/hobby project. @@ -34,8 +35,6 @@ A Rust library for Discord Rich Presence that actually works without the headach - [x] Async support with runtime-agnostic design - [x] Support for tokio, async-std, and smol - [x] Flexible pipe/socket selection -- [ ] Error handling could be better -- [ ] Party/lobby features (partial implementation only) ## Quick Start @@ -358,7 +357,7 @@ match client.connect() { - [ ] Party/lobby functionality (partial implementation) - [x] Async support (tokio, async-std, and smol) - [x] More comprehensive examples -- [ ] Publish to crates.io +- [x] Publish to crates.io - [ ] CI/CD pipeline - [x] Proper documentation - [x] Connection retry logic with exponential backoff diff --git a/changelogs/unreleased.md b/changelogs/0.1.0-dev.md similarity index 100% rename from changelogs/unreleased.md rename to changelogs/0.1.0-dev.md From ad3db2d133fec5311c8b52c44efe714f8d499183 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:10:30 +0530 Subject: [PATCH 26/62] chroe: applied suggetion from clippy and fmt --- examples/async_tokio_reconnect.rs | 4 +- examples/connection_retry.rs | 2 +- src/activity/types.rs | 61 +++++++++++++++---------------- src/async_io/async_std/mod.rs | 6 +-- src/async_io/client.rs | 34 ++++++++--------- src/async_io/smol/mod.rs | 10 ++--- src/async_io/tokio/mod.rs | 8 ++-- src/async_io/traits.rs | 4 +- src/ipc/connection.rs | 6 +-- src/sync/client.rs | 32 ++++++++-------- tests/integration_retry.rs | 4 +- 11 files changed, 84 insertions(+), 87 deletions(-) diff --git a/examples/async_tokio_reconnect.rs b/examples/async_tokio_reconnect.rs index c4cced2..5a10e4b 100644 --- a/examples/async_tokio_reconnect.rs +++ b/examples/async_tokio_reconnect.rs @@ -1,9 +1,9 @@ #[cfg(feature = "tokio-runtime")] mod tokio_example { use clap::Parser; - use presenceforge::retry::{with_retry_async, RetryConfig}; + use presenceforge::retry::{RetryConfig, with_retry_async}; use presenceforge::{ActivityBuilder, AsyncDiscordIpcClient, Result}; - use tokio::time::{sleep, Duration}; + use tokio::time::{Duration, sleep}; /// Discord Rich Presence Async Tokio Reconnection Example #[derive(Parser, Debug)] diff --git a/examples/connection_retry.rs b/examples/connection_retry.rs index 9cd438b..ec08f36 100644 --- a/examples/connection_retry.rs +++ b/examples/connection_retry.rs @@ -1,5 +1,5 @@ use clap::Parser; -use presenceforge::retry::{with_retry, RetryConfig}; +use presenceforge::retry::{RetryConfig, with_retry}; use presenceforge::sync::DiscordIpcClient; use presenceforge::{ActivityBuilder, Result}; use std::time::Duration; diff --git a/src/activity/types.rs b/src/activity/types.rs index 61af9a6..229cae7 100644 --- a/src/activity/types.rs +++ b/src/activity/types.rs @@ -37,16 +37,16 @@ impl Activity { /// Ok(()) if valid, or Err(String) with the reason if invalid pub fn validate(&self) -> Result<(), String> { // Check text field lengths - if let Some(state) = &self.state { - if state.len() > 128 { - return Err("State must be 128 characters or less".to_string()); - } + if let Some(state) = &self.state + && state.len() > 128 + { + return Err("State must be 128 characters or less".to_string()); } - if let Some(details) = &self.details { - if details.len() > 128 { - return Err("Details must be 128 characters or less".to_string()); - } + if let Some(details) = &self.details + && details.len() > 128 + { + return Err("Details must be 128 characters or less".to_string()); } // Validate buttons @@ -74,40 +74,37 @@ impl Activity { // Validate asset keys if let Some(assets) = &self.assets { - if let Some(large_image) = &assets.large_image { - if large_image.len() > 256 { - return Err("Large image key must be 256 characters or less".to_string()); - } + if let Some(large_image) = &assets.large_image + && large_image.len() > 256 + { + return Err("Large image key must be 256 characters or less".to_string()); } - if let Some(small_image) = &assets.small_image { - if small_image.len() > 256 { - return Err("Small image key must be 256 characters or less".to_string()); - } + if let Some(small_image) = &assets.small_image + && small_image.len() > 256 + { + return Err("Small image key must be 256 characters or less".to_string()); } - if let Some(large_text) = &assets.large_text { - if large_text.len() > 128 { - return Err("Large text must be 128 characters or less".to_string()); - } + if let Some(large_text) = &assets.large_text + && large_text.len() > 128 + { + return Err("Large text must be 128 characters or less".to_string()); } - if let Some(small_text) = &assets.small_text { - if small_text.len() > 128 { - return Err("Small text must be 128 characters or less".to_string()); - } + if let Some(small_text) = &assets.small_text + && small_text.len() > 128 + { + return Err("Small text must be 128 characters or less".to_string()); } } // Validate party size - if let Some(party) = &self.party { - if let Some(size) = &party.size { - if size[0] > size[1] { - return Err( - "Current party size cannot be greater than max party size".to_string() - ); - } - } + if let Some(party) = &self.party + && let Some(size) = &party.size + && size[0] > size[1] + { + return Err("Current party size cannot be greater than max party size".to_string()); } Ok(()) diff --git a/src/async_io/async_std/mod.rs b/src/async_io/async_std/mod.rs index f252873..3012ee4 100644 --- a/src/async_io/async_std/mod.rs +++ b/src/async_io/async_std/mod.rs @@ -21,7 +21,7 @@ use std::sync::{Arc, Mutex}; use crate::async_io::traits::{AsyncRead, AsyncWrite}; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; -use crate::ipc::{constants, PipeConfig}; +use crate::ipc::{PipeConfig, constants}; /// A Discord IPC connection using async-std pub(crate) enum AsyncStdConnection { @@ -142,7 +142,7 @@ impl AsyncStdConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC socket. Check file permissions." + "Permission denied when connecting to Discord IPC socket. Check file permissions.", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) @@ -224,7 +224,7 @@ impl AsyncStdConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?" + "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) diff --git a/src/async_io/client.rs b/src/async_io/client.rs index 8217bbb..cf8a7a3 100644 --- a/src/async_io/client.rs +++ b/src/async_io/client.rs @@ -1,17 +1,17 @@ //! Async Discord IPC Client implementation use bytes::{BufMut, BytesMut}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::VecDeque; use std::process; use std::time::{Duration, Instant}; use super::traits::ipc_utils::read_u32_le; -use super::traits::{read_exact, write_all, AsyncRead, AsyncWrite}; +use super::traits::{AsyncRead, AsyncWrite, read_exact, write_all}; use crate::activity::Activity; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; -use crate::ipc::{constants, Command, HandshakePayload, IpcMessage, Opcode}; +use crate::ipc::{Command, HandshakePayload, IpcMessage, Opcode, constants}; use crate::nonce::generate_nonce; /// Async implementation of Discord IPC client @@ -155,13 +155,13 @@ where } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) + && resp_nonce != nonce + { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); } Ok(()) @@ -219,13 +219,13 @@ where } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) + && resp_nonce != nonce + { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); } Ok(response) diff --git a/src/async_io/smol/mod.rs b/src/async_io/smol/mod.rs index a7c8e9c..0cbb9e7 100644 --- a/src/async_io/smol/mod.rs +++ b/src/async_io/smol/mod.rs @@ -19,7 +19,7 @@ use std::sync::{Arc, Mutex}; use crate::async_io::traits::{AsyncRead, AsyncWrite}; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; -use crate::ipc::{constants, PipeConfig}; +use crate::ipc::{PipeConfig, constants}; /// A Discord IPC connection using smol pub(crate) enum SmolConnection { @@ -149,7 +149,7 @@ impl SmolConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC socket. Check file permissions." + "Permission denied when connecting to Discord IPC socket. Check file permissions.", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) @@ -232,7 +232,7 @@ impl SmolConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?" + "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) @@ -447,8 +447,8 @@ pub mod client { /// Performs handshake with Discord with a timeout pub async fn connect_with_timeout(&mut self, timeout_duration: Duration) -> Result { - use smol::future::or; use smol::Timer; + use smol::future::or; match or( async move { @@ -509,8 +509,8 @@ pub mod client { &mut self, timeout_duration: Duration, ) -> impl std::future::Future> + Send { - use smol::future::or; use smol::Timer; + use smol::future::or; async move { match or( diff --git a/src/async_io/tokio/mod.rs b/src/async_io/tokio/mod.rs index 7864cd5..e848fc6 100644 --- a/src/async_io/tokio/mod.rs +++ b/src/async_io/tokio/mod.rs @@ -14,7 +14,7 @@ use tokio::net::windows::named_pipe::{ClientOptions, NamedPipeClient}; use crate::async_io::traits::{AsyncRead, AsyncWrite}; use crate::error::{DiscordIpcError, Result}; -use crate::ipc::{constants, PipeConfig}; +use crate::ipc::{PipeConfig, constants}; /// A Discord IPC connection using Tokio pub(crate) enum TokioConnection { @@ -46,7 +46,7 @@ impl TokioConnection { config: Option, timeout_ms: u64, ) -> Result { - use tokio::time::{timeout, Duration}; + use tokio::time::{Duration, timeout}; let timeout_duration = Duration::from_millis(timeout_ms); @@ -122,7 +122,7 @@ impl TokioConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC socket. Check file permissions." + "Permission denied when connecting to Discord IPC socket. Check file permissions.", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) @@ -174,7 +174,7 @@ impl TokioConnection { if err.kind() == io::ErrorKind::PermissionDenied { Err(DiscordIpcError::ConnectionFailed(io::Error::new( io::ErrorKind::PermissionDenied, - "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?" + "Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?", ))) } else { Err(DiscordIpcError::ConnectionFailed(err)) diff --git a/src/async_io/traits.rs b/src/async_io/traits.rs index d63db7a..57eed7b 100644 --- a/src/async_io/traits.rs +++ b/src/async_io/traits.rs @@ -41,7 +41,7 @@ pub async fn read_exact( return Err(io::Error::new( io::ErrorKind::UnexpectedEof, "failed to fill buffer", - )) + )); } Ok(n) => buf = &mut buf[n..], Err(e) => return Err(e), @@ -91,7 +91,7 @@ pub async fn write_all( return Err(io::Error::new( io::ErrorKind::WriteZero, "failed to write whole buffer", - )) + )); } Ok(n) => buf = &buf[n..], Err(e) => return Err(e), diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 047cffc..d5a4ff8 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -12,7 +12,7 @@ use std::fs::OpenOptions; use std::io::{BufReader, BufWriter}; use crate::error::{DiscordIpcError, ProtocolContext, Result}; -use crate::ipc::protocol::{constants, Opcode}; +use crate::ipc::protocol::{Opcode, constants}; /// Configuration for selecting which Discord IPC pipe to connect to #[derive(Debug, Clone, Default)] @@ -364,8 +364,8 @@ impl IpcConnection { #[cfg(windows)] /// Connect to Discord IPC named pipe on Windows using auto-discovery - fn connect_to_discord_windows_auto( - ) -> Result<(BufReader, BufWriter)> { + fn connect_to_discord_windows_auto() + -> Result<(BufReader, BufWriter)> { let mut last_error = None; let mut attempted_paths = Vec::new(); diff --git a/src/sync/client.rs b/src/sync/client.rs index 01ec0ca..6f4252e 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -1,4 +1,4 @@ -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::VecDeque; use std::process; use std::time::{Duration, Instant}; @@ -7,7 +7,7 @@ use crate::activity::Activity; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; use crate::ipc::{ - constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, + Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, constants, }; use crate::nonce::generate_nonce; @@ -236,13 +236,13 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) + && resp_nonce != nonce + { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); } Ok(()) @@ -300,13 +300,13 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { - if resp_nonce != nonce { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); - } + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) + && resp_nonce != nonce + { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); } Ok(response) diff --git a/tests/integration_retry.rs b/tests/integration_retry.rs index 5bc70e8..fa0cd44 100644 --- a/tests/integration_retry.rs +++ b/tests/integration_retry.rs @@ -1,7 +1,7 @@ use presenceforge::retry::with_retry; -use presenceforge::{retry::RetryConfig, DiscordIpcError}; -use std::sync::atomic::{AtomicUsize, Ordering}; +use presenceforge::{DiscordIpcError, retry::RetryConfig}; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; fn quick_retry_config(max_attempts: u32) -> RetryConfig { RetryConfig::new(max_attempts, 5, 20, 2.0) From 14b02b7dc3d4d0860d9ac467433c984a6984f8cb Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:19:05 +0530 Subject: [PATCH 27/62] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sync/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sync/client.rs b/src/sync/client.rs index 6f4252e..c1dc795 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -206,6 +206,7 @@ impl DiscordIpcClient { }; let payload = serde_json::to_value(message)?; + #[cfg(debug_assertions)] debug_println!("[PAYLOAD]: {:?} ", payload); self.connection.send(Opcode::Frame, &payload)?; From 6068536dc925b939219670884c67eb9110b215cb Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:21:35 +0530 Subject: [PATCH 28/62] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index d5a4ff8..25bdc80 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -411,7 +411,6 @@ impl IpcConnection { pub fn send(&mut self, opcode: Opcode, payload: &Value) -> Result<()> { let raw = serde_json::to_vec(payload)?; // Clear and prepare write buffer - // debug_println!("[RAW] : {raw:?}"); self.write_buf.clear(); self.write_buf.reserve(8 + raw.len()); From ed2e0abecb4b7ab36fe9f291809699b2bfaf07a6 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:21:59 +0530 Subject: [PATCH 29/62] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 25bdc80..cadef68 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -90,7 +90,7 @@ impl IpcConnection { unsafe { libc::getuid() } } /// Discovers potential base directories where IPC sockets may exist - /// Check enviroment variables + /// Check environment variables /// - `XDG_RUNTIME_DIR` /// - `TMPDIR` /// - `TMP` From 283863545851290cc8e7c6547e5e5a86211cbceb Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:22:10 +0530 Subject: [PATCH 30/62] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index cadef68..92ca09a 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -95,7 +95,7 @@ impl IpcConnection { /// - `TMPDIR` /// - `TMP` /// - `TEMP` - /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specfic + /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specific /// - if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user /// - `/run/user/{UID}` #[cfg(unix)] From 6057a7fe008030e12db03d0763c3f8b7d4ac7f7f Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:28:49 +0530 Subject: [PATCH 31/62] Update src/macros.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index f3748ce..5a6fd1f 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -7,7 +7,7 @@ static DEBUG_ENABLED: OnceLock = OnceLock::new(); pub fn is_debug_enabled() -> bool { *DEBUG_ENABLED.get_or_init(|| { std::env::var("PRESENCEFORGE_DEBUG") - .map(|val| val == "1" || val.to_lowercase() == "true") + .map(|val| val == "1" || val.eq_ignore_ascii_case("true")) .unwrap_or(false) }) } From ec50ddb05019c3fd94d77bf3b1258559a8175e70 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:28:59 +0530 Subject: [PATCH 32/62] Update scripts/clippy_god_mode.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- scripts/clippy_god_mode.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/clippy_god_mode.sh b/scripts/clippy_god_mode.sh index 5d1a4a8..771f5aa 100755 --- a/scripts/clippy_god_mode.sh +++ b/scripts/clippy_god_mode.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh -cd .. # assuming you are runing from scripts directory +cd .. # assuming you are running from scripts directory cargo clippy --all-targets --all-features -- -W clippy::pedantic -W clippy::nursery From 9648e1f0219aad03c18bd1806b94b1485a9db713 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:32:39 +0530 Subject: [PATCH 33/62] chore: fix docs and improve spacing --- docs/ERROR_HANDLING.md | 2 +- docs/FAQ.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md index 9a9af52..fa8cb78 100644 --- a/docs/ERROR_HANDLING.md +++ b/docs/ERROR_HANDLING.md @@ -231,7 +231,7 @@ fn connect_to_discord(client_id: &str) -> Result Date: Sat, 18 Oct 2025 15:36:05 +0530 Subject: [PATCH 34/62] Update src/ipc/connection.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 92ca09a..97e588d 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -95,6 +95,7 @@ impl IpcConnection { /// - `TMPDIR` /// - `TMP` /// - `TEMP` + /// - `tmp` /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specific /// - if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user /// - `/run/user/{UID}` From 3328e5a501d18b11e474a4115fa76f126b9671e7 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:36:18 +0530 Subject: [PATCH 35/62] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1db4e2f..b5be588 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features ```rust use presenceforge::ActivityBuilder; -use presenceforge::sync::DiscordIpcClient +use presenceforge::sync::DiscordIpcClient; fn main() -> Result<(), Box> { let mut client = DiscordIpcClient::new("your_client_id")?; client.connect()?; From 52e8ee83365c24cb01fb5f83d8bee45e856b2110 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:37:38 +0530 Subject: [PATCH 36/62] Update src/nonce.rs fine Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/nonce.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonce.rs b/src/nonce.rs index 8b0f3dc..0fd8164 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -25,7 +25,7 @@ use uuid::Uuid; /// # Security /// /// UUID v4 provides 122 bits of randomness, making collisions extremely unlikely -/// probability of collision is approximately 1 in 2^61 after generating 1 billion UUID :) +/// probability of collision is approximately 1 in 2^61 after generating 1 billion UUID. pub fn generate_nonce(prefix: &str) -> String { format!("{}-{}", prefix, Uuid::new_v4()) } From 58bb88a48e1a39dec2bdbcf0f9166c3464ec109e Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:39:11 +0530 Subject: [PATCH 37/62] chore : removed a extra whitespace --- docs/FAQ.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index f873623..18e0ec0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -160,7 +160,7 @@ Error: ProtocolError("Handshake failed") ```rust use presenceforge::{IpcConnection, PipeConfig}; - use presenceforge:: DiscordIpcClient; + use presenceforge::DiscordIpcClient; let pipes = IpcConnection::discover_pipes(); for pipe in pipes { println!("Trying pipe: {}", pipe.path); From 28e06cd2e337340017c86eddaf62cd1eda8fe57b Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:42:53 +0530 Subject: [PATCH 38/62] Update src/ipc/connection.rs formated the result type to appear on the same line Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 97e588d..96a3ece 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -365,8 +365,7 @@ impl IpcConnection { #[cfg(windows)] /// Connect to Discord IPC named pipe on Windows using auto-discovery - fn connect_to_discord_windows_auto() - -> Result<(BufReader, BufWriter)> { + fn connect_to_discord_windows_auto() -> Result<(BufReader, BufWriter)> { let mut last_error = None; let mut attempted_paths = Vec::new(); From 5865426d7e6a8539216ce380e40c43f3e728cb21 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:49:38 +0530 Subject: [PATCH 39/62] Update src/sync/client.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sync/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync/client.rs b/src/sync/client.rs index c1dc795..33f28bc 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -1,4 +1,4 @@ -use serde_json::{Value, json}; +use serde_json::{json, Value}; use std::collections::VecDeque; use std::process; use std::time::{Duration, Instant}; From 024469101dde17ba6a6febf18a56b43a3d34a31f Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:50:03 +0530 Subject: [PATCH 40/62] Update src/sync/client.rs fixed import order acc to copiolt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sync/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync/client.rs b/src/sync/client.rs index 33f28bc..bf14da2 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -7,7 +7,7 @@ use crate::activity::Activity; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; use crate::ipc::{ - Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, constants, + constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, }; use crate::nonce::generate_nonce; From 3ce87b37bc785595cf8b7fe8fcf288bf39e05bbb Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:52:46 +0530 Subject: [PATCH 41/62] Update src/ipc/connection.rs improve consistency in docs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 96a3ece..49cb7f5 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -96,9 +96,9 @@ impl IpcConnection { /// - `TMP` /// - `TEMP` /// - `tmp` - /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` -> flatpak specific - /// - if XDG_RUNTIME_DIR is not set the function will grab the uid of the current user - /// - `/run/user/{UID}` + /// - `XDG_RUNTIME_DIR/app/com.discordapp.Discord` (Flatpak specific) + /// - `/run/user/{UID}` (if `XDG_RUNTIME_DIR` is not set) + /// - `/run/user/{UID}/app/com.discordapp.Discord` (Flatpak fallback) #[cfg(unix)] fn candidate_ipc_dir() -> Vec { let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP", "tmp"]; From 996049c15f4c9789ecc46cfd2e3a4e75e7a2da04 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:53:17 +0530 Subject: [PATCH 42/62] Update src/retry.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/retry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/retry.rs b/src/retry.rs index 9ded736..5f26860 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -403,7 +403,7 @@ fn test_retry_exhausts_attempts() { let config = RetryConfig::with_max_attempts(3); let mut attempt_count = 0; - let result: std::result::Result<(), DiscordIpcError> = with_retry(&config, || { + let result = with_retry(&config, || { attempt_count += 1; // SocketClosed is recoverable Err(DiscordIpcError::SocketClosed) From 9391ee9c42a5dc530eff7b75c7848e4347485580 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 15:54:27 +0530 Subject: [PATCH 43/62] Update src/sync/client.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sync/client.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sync/client.rs b/src/sync/client.rs index bf14da2..6b3557e 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -237,9 +237,12 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) - && resp_nonce != nonce - { + let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) else { + return Err(DiscordIpcError::InvalidResponse( + "Missing nonce in response".to_string(), + )); + }; + if resp_nonce != nonce { return Err(DiscordIpcError::InvalidResponse(format!( "Nonce mismatch: expected {}, got {}", nonce, resp_nonce From 896754969f25b713ee7d7806408906169a39dd6e Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:54:57 +0530 Subject: [PATCH 44/62] chore: cargo fmt --- src/ipc/connection.rs | 3 ++- src/sync/client.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 49cb7f5..ca6a7e7 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -365,7 +365,8 @@ impl IpcConnection { #[cfg(windows)] /// Connect to Discord IPC named pipe on Windows using auto-discovery - fn connect_to_discord_windows_auto() -> Result<(BufReader, BufWriter)> { + fn connect_to_discord_windows_auto() + -> Result<(BufReader, BufWriter)> { let mut last_error = None; let mut attempted_paths = Vec::new(); diff --git a/src/sync/client.rs b/src/sync/client.rs index 6b3557e..60d8155 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -1,4 +1,4 @@ -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::VecDeque; use std::process; use std::time::{Duration, Instant}; @@ -7,7 +7,7 @@ use crate::activity::Activity; use crate::debug_println; use crate::error::{DiscordIpcError, Result}; use crate::ipc::{ - constants, Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, + Command, HandshakePayload, IpcConnection, IpcMessage, Opcode, PipeConfig, constants, }; use crate::nonce::generate_nonce; From e9dd98d17782246f5898350b476f9e17a92a7748 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 15:57:46 +0530 Subject: [PATCH 45/62] dev: added explicity type to closure return and formated the code --- src/retry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/retry.rs b/src/retry.rs index 5f26860..9ded736 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -403,7 +403,7 @@ fn test_retry_exhausts_attempts() { let config = RetryConfig::with_max_attempts(3); let mut attempt_count = 0; - let result = with_retry(&config, || { + let result: std::result::Result<(), DiscordIpcError> = with_retry(&config, || { attempt_count += 1; // SocketClosed is recoverable Err(DiscordIpcError::SocketClosed) From 918e51daa9283dae9c274fbeb7e94017a50eb99b Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 16:39:05 +0530 Subject: [PATCH 46/62] chore: enabled party feature for builder_all.rs --- examples/builder_all.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/builder_all.rs b/examples/builder_all.rs index a327473..8387efe 100644 --- a/examples/builder_all.rs +++ b/examples/builder_all.rs @@ -87,7 +87,7 @@ fn main() -> Result { // Party: Shows "X of Y" (e.g., "2 of 4" for a party) // Useful for multiplayer games showing current players // Parameters: party_id, current_size, max_size - // .party("party-12345", 2, 4) + .party("party-12345", 2, 4) // ═══════════════════════════════════════════════════════════ // BUTTONS (Clickable buttons - max 2) // ═══════════════════════════════════════════════════════════ From 400fb2507400ac8ebf0a1a968f4cf89c9e47ec9b Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 16:58:14 +0530 Subject: [PATCH 47/62] chore: bump the release of changelogs to 0.1.0-dev --- changelogs/0.1.0-dev.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelogs/0.1.0-dev.md b/changelogs/0.1.0-dev.md index b2acb39..f01f23c 100644 --- a/changelogs/0.1.0-dev.md +++ b/changelogs/0.1.0-dev.md @@ -1,8 +1,7 @@ # Unreleased -## [0.0.0] - Unreleased +## [0.1.0-dev] - dev-release -> **WARNING:** Early development version. Not recommended for production use. ### Added From 9e38d4cd89e8bdeac9a9ac59aea45494535c02d5 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:03:16 +0530 Subject: [PATCH 48/62] Update README for presenceforge dependency version --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b5be588..3ae053e 100644 --- a/README.md +++ b/README.md @@ -42,21 +42,20 @@ Add PresenceForge to your `Cargo.toml`: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge" } +presenceforge = "0.1.0-dev" ``` For async support, add one of the runtime features: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } # OR -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["async-std-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["async-std-runtime"] } # OR -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["smol-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["smol-runtime"] } ``` -> **Note**: Not published to crates.io yet. Use the git dependency for now. ### Basic Usage (Synchronous) From b007233732526bba51217d3f7ac3d03086e42986 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 17:15:59 +0530 Subject: [PATCH 49/62] docs: bumped all the example version to 0.1.0-dev --- docs/ACTIVITY_BUILDER_REFERENCE.md | 2 +- docs/ASYNC_RUNTIMES.md | 12 ++++++------ docs/FAQ.md | 11 ++++++----- docs/GETTING_STARTED.md | 12 +++++------- src/lib.rs | 6 +++--- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/ACTIVITY_BUILDER_REFERENCE.md b/docs/ACTIVITY_BUILDER_REFERENCE.md index f014454..9abc6af 100644 --- a/docs/ACTIVITY_BUILDER_REFERENCE.md +++ b/docs/ACTIVITY_BUILDER_REFERENCE.md @@ -174,7 +174,7 @@ let end_time = now + 300; ### 8. **Secrets** (For "Ask to Join" and Spectate features) > **⚠️ Feature Flag Required:** These methods require the `secrets` feature flag to be enabled. -> Add to your `Cargo.toml`: `presenceforge = { git = "...", features = ["secrets"] }` +> Add to your `Cargo.toml`: `presenceforge = { version = "0.1.0-dev", features = ["secrets"] }` #### Note: untested feature diff --git a/docs/ASYNC_RUNTIMES.md b/docs/ASYNC_RUNTIMES.md index 3313f61..739f5b5 100644 --- a/docs/ASYNC_RUNTIMES.md +++ b/docs/ASYNC_RUNTIMES.md @@ -63,13 +63,13 @@ Add to your `Cargo.toml` with **one** of these feature flags: ```toml [dependencies] # For Tokio -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } # For async-std -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["async-std-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["async-std-runtime"] } # For smol -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["smol-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["smol-runtime"] } ``` **This exact code works with all three runtimes:** @@ -140,7 +140,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` @@ -320,7 +320,7 @@ async-std provides an async API similar to the standard library. ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["async-std-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["async-std-runtime"] } async-std = { version = "1", features = ["attributes"] } ``` @@ -419,7 +419,7 @@ smol is a small and fast async runtime. ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["smol-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["smol-runtime"] } smol = "2" ``` diff --git a/docs/FAQ.md b/docs/FAQ.md index 18e0ec0..f4aa201 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -2,7 +2,6 @@ Frequently asked questions and solutions to common problems. - ## Table of Contents - [General Questions](#general-questions) @@ -51,11 +50,13 @@ git ls-remote https://github.com/Sreehari425/presenceforge.git git ls-remote git@github.com:Sreehari425/presenceforge.git ``` -If SSH works, use this in `Cargo.toml`: +If SSH works but you still have issues, please report them on GitHub Issues. + +For now, use version `0.1.0-dev` instead: ```toml [dependencies] -presenceforge = { git = "ssh://git@github.com/Sreehari425/presenceforge.git" } +presenceforge = "0.1.0-dev" ``` --- @@ -73,7 +74,7 @@ The feature is called `tokio-runtime`, not `tokio`: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } ``` Valid features: `tokio-runtime`, `async-std-runtime`, `smol-runtime` @@ -93,7 +94,7 @@ Make sure you're using compatible versions: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } tokio = { version = "1", features = ["full"] } # Use version 1.x ``` diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 5f4f4ec..0bf0ff5 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -36,11 +36,9 @@ Add PresenceForge to your `Cargo.toml`: ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge" } +presenceforge = "0.1.0-dev" ``` -> **Note**: PresenceForge is not yet published to crates.io. Use the git dependency for now. - ### With Async Support If you need async support, add one of the runtime features: @@ -48,13 +46,13 @@ If you need async support, add one of the runtime features: ```toml [dependencies] # For Tokio users -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["tokio-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } # For async-std users -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["async-std-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["async-std-runtime"] } # For smol users -presenceforge = { git = "https://github.com/Sreehari425/presenceforge", features = ["smol-runtime"] } +presenceforge = { version = "0.1.0-dev", features = ["smol-runtime"] } ``` ## Your First Rich Presence @@ -72,7 +70,7 @@ cd my-discord-presence ```toml [dependencies] -presenceforge = { git = "https://github.com/Sreehari425/presenceforge" } +presenceforge = "0.1.0-dev" ``` ### Step 3: Write your first presence diff --git a/src/lib.rs b/src/lib.rs index 371174b..862b118 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ //! //! ```toml //! [dependencies] -//! presenceforge = { version = "0.0.0", features = ["tokio-runtime"] } +//! presenceforge = { version = "0.1.0-dev", features = ["tokio-runtime"] } //! tokio = { version = "1", features = ["rt-multi-thread", "macros"] } //! ``` //! @@ -87,7 +87,7 @@ //! //! ```toml //! [dependencies] -//! presenceforge = { version = "0.0.0", features = ["async-std-runtime"] } +//! presenceforge = { version = "0.1.0-dev", features = ["async-std-runtime"] } //! async-std = { version = "1", features = ["attributes"] } //! ``` //! @@ -122,7 +122,7 @@ //! //! ```toml //! [dependencies] -//! presenceforge = { version = "0.0.0", features = ["smol-runtime"] } +//! presenceforge = { version = "0.1.0-dev", features = ["smol-runtime"] } //! smol = "2" //! ``` //! From eb756b13ff2a706ce75753aad6830c4c8fbac6ec Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:31:39 +0530 Subject: [PATCH 50/62] Update src/nonce.rs gramer-fix Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/nonce.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nonce.rs b/src/nonce.rs index 0fd8164..aeed27c 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -25,7 +25,7 @@ use uuid::Uuid; /// # Security /// /// UUID v4 provides 122 bits of randomness, making collisions extremely unlikely -/// probability of collision is approximately 1 in 2^61 after generating 1 billion UUID. +/// probability of collision is approximately 1 in 2^61 after generating 1 billion UUIDs. pub fn generate_nonce(prefix: &str) -> String { format!("{}-{}", prefix, Uuid::new_v4()) } From 7cc6f3a4cbddac65a0fa6f19a65546f8e79f4e0e Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:33:00 +0530 Subject: [PATCH 51/62] added docs to current_uuid function Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index ca6a7e7..769fc2b 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -86,6 +86,13 @@ impl IpcConnection { // Returns the current users UID on unix based systems #[cfg(unix)] + /// Returns the current user's UID on Unix-based systems. + /// + /// # Safety + /// + /// This function calls `libc::getuid()` inside an `unsafe` block. According to the POSIX standard, + /// `getuid()` is always safe to call: it takes no arguments, does not dereference pointers, and + /// cannot cause undefined behavior. Therefore, this usage of `unsafe` is sound. fn current_uid() -> u32 { unsafe { libc::getuid() } } From f285dd597b766eee08af2f7da8dc89a00ef3f44e Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 18:40:12 +0530 Subject: [PATCH 52/62] dev: used if-let syntax for consisteny --- src/sync/client.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/sync/client.rs b/src/sync/client.rs index 60d8155..c1dc795 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -237,12 +237,9 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) else { - return Err(DiscordIpcError::InvalidResponse( - "Missing nonce in response".to_string(), - )); - }; - if resp_nonce != nonce { + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) + && resp_nonce != nonce + { return Err(DiscordIpcError::InvalidResponse(format!( "Nonce mismatch: expected {}, got {}", nonce, resp_nonce From c9a1fac5cbd96218ebe1936a77f1925e3b9e2ab9 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:45:15 +0530 Subject: [PATCH 53/62] Update src/sync/client.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/sync/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sync/client.rs b/src/sync/client.rs index c1dc795..c7fb3e6 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -207,6 +207,7 @@ impl DiscordIpcClient { let payload = serde_json::to_value(message)?; #[cfg(debug_assertions)] + // Intentional: Print payload for debugging in debug builds only. debug_println!("[PAYLOAD]: {:?} ", payload); self.connection.send(Opcode::Frame, &payload)?; From fd81f49f839a44607187917fa866e67481b139c7 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:45:53 +0530 Subject: [PATCH 54/62] Update src/ipc/connection.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 769fc2b..fa26b59 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -372,8 +372,7 @@ impl IpcConnection { #[cfg(windows)] /// Connect to Discord IPC named pipe on Windows using auto-discovery - fn connect_to_discord_windows_auto() - -> Result<(BufReader, BufWriter)> { + fn connect_to_discord_windows_auto() -> Result<(BufReader, BufWriter)> { let mut last_error = None; let mut attempted_paths = Vec::new(); From 52757575040eedafdcff3c45d8756fed99a99cd3 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 18:46:27 +0530 Subject: [PATCH 55/62] chore: cargo fmt --- src/ipc/connection.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index fa26b59..769fc2b 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -372,7 +372,8 @@ impl IpcConnection { #[cfg(windows)] /// Connect to Discord IPC named pipe on Windows using auto-discovery - fn connect_to_discord_windows_auto() -> Result<(BufReader, BufWriter)> { + fn connect_to_discord_windows_auto() + -> Result<(BufReader, BufWriter)> { let mut last_error = None; let mut attempted_paths = Vec::new(); From e3955247397b1000af389d6c2138ddc03e44af03 Mon Sep 17 00:00:00 2001 From: SreehariAnil255 <69724114+Sreehari425@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:52:51 +0530 Subject: [PATCH 56/62] Update src/ipc/connection.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/ipc/connection.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ipc/connection.rs b/src/ipc/connection.rs index 769fc2b..1eb7eb9 100644 --- a/src/ipc/connection.rs +++ b/src/ipc/connection.rs @@ -88,11 +88,7 @@ impl IpcConnection { #[cfg(unix)] /// Returns the current user's UID on Unix-based systems. /// - /// # Safety - /// - /// This function calls `libc::getuid()` inside an `unsafe` block. According to the POSIX standard, - /// `getuid()` is always safe to call: it takes no arguments, does not dereference pointers, and - /// cannot cause undefined behavior. Therefore, this usage of `unsafe` is sound. + /// Safety: Calling `libc::getuid()` is always safe per POSIX. fn current_uid() -> u32 { unsafe { libc::getuid() } } From 1b150b7ba20761f2edcc647473a22bfbb5501e4c Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 19:31:39 +0530 Subject: [PATCH 57/62] chore : minor typo fix --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index fa59623..20b71ba 100644 --- a/examples/README.md +++ b/examples/README.md @@ -186,7 +186,7 @@ Advanced pipe discovery example showing: Error handling and recovery example (synchronous) showing: -- Basic retry with `with_retry()` function +- Basic retry with `with_retry()` functions - Manual reconnection using `reconnect()` method - Custom retry configuration (max attempts, delays, backoff) - Handling recoverable vs non-recoverable errors From 83597f6acdc8e44d5e6cb4abf95888edb284f2ec Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 19:35:31 +0530 Subject: [PATCH 58/62] Trigger CI for PR From 611dbf23217f7e59eccc1879c9f8e13e9dcc7fbb Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sat, 18 Oct 2025 19:43:01 +0530 Subject: [PATCH 59/62] chore: copied the ci from main --- .github/workflows/basic-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/basic-ci.yml b/.github/workflows/basic-ci.yml index 71ada76..396a299 100644 --- a/.github/workflows/basic-ci.yml +++ b/.github/workflows/basic-ci.yml @@ -1,14 +1,14 @@ name: CI + on: push: branches: - main + pull_request: + branches: + - main workflow_dispatch: - -permissions: - contents: read - env: CARGO_TERM_COLOR: always From ca90ba9179f31fb9372279c4f577833dd962704a Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sun, 19 Oct 2025 13:38:57 +0530 Subject: [PATCH 60/62] scripts: added tree script --- scripts/project_tree.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 scripts/project_tree.sh diff --git a/scripts/project_tree.sh b/scripts/project_tree.sh new file mode 100755 index 0000000..d43fbcc --- /dev/null +++ b/scripts/project_tree.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +cd .. # assuming you are running from scripts directory +tree -a -I '.git' --gitignore From fabb33fb24c612c82339d21e25cd8dd282165a76 Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sun, 19 Oct 2025 21:44:52 +0530 Subject: [PATCH 61/62] Lowered MSRV Co-authored-by: mrmayman --- src/activity/types.rs | 57 +++++++++++++++++++++--------------------- src/async_io/client.rs | 28 ++++++++++----------- src/sync/client.rs | 28 ++++++++++----------- 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/activity/types.rs b/src/activity/types.rs index 229cae7..173fa12 100644 --- a/src/activity/types.rs +++ b/src/activity/types.rs @@ -37,16 +37,16 @@ impl Activity { /// Ok(()) if valid, or Err(String) with the reason if invalid pub fn validate(&self) -> Result<(), String> { // Check text field lengths - if let Some(state) = &self.state - && state.len() > 128 - { - return Err("State must be 128 characters or less".to_string()); + if let Some(state) = &self.state { + if state.len() > 128 { + return Err("State must be 128 characters or less".to_string()); + } } - if let Some(details) = &self.details - && details.len() > 128 - { - return Err("Details must be 128 characters or less".to_string()); + if let Some(details) = &self.details { + if details.len() > 128 { + return Err("Details must be 128 characters or less".to_string()); + } } // Validate buttons @@ -74,37 +74,36 @@ impl Activity { // Validate asset keys if let Some(assets) = &self.assets { - if let Some(large_image) = &assets.large_image - && large_image.len() > 256 - { - return Err("Large image key must be 256 characters or less".to_string()); + if let Some(large_image) = &assets.large_image { + if large_image.len() > 256 { + return Err("Large image key must be 256 characters or less".to_string()); + } } - if let Some(small_image) = &assets.small_image - && small_image.len() > 256 - { - return Err("Small image key must be 256 characters or less".to_string()); + if let Some(small_image) = &assets.small_image { + if small_image.len() > 256 { + return Err("Small image key must be 256 characters or less".to_string()); + } } - if let Some(large_text) = &assets.large_text - && large_text.len() > 128 - { - return Err("Large text must be 128 characters or less".to_string()); + if let Some(large_text) = &assets.large_text { + if large_text.len() > 128 { + return Err("Large text must be 128 characters or less".to_string()); + } } - if let Some(small_text) = &assets.small_text - && small_text.len() > 128 - { - return Err("Small text must be 128 characters or less".to_string()); + if let Some(small_text) = &assets.small_text { + if small_text.len() > 128 { + return Err("Small text must be 128 characters or less".to_string()); + } } } // Validate party size - if let Some(party) = &self.party - && let Some(size) = &party.size - && size[0] > size[1] - { - return Err("Current party size cannot be greater than max party size".to_string()); + if let Some(size) = self.party.as_ref().and_then(|n| n.size) { + if size[0] > size[1] { + return Err("Current party size cannot be greater than max party size".to_string()); + } } Ok(()) diff --git a/src/async_io/client.rs b/src/async_io/client.rs index cf8a7a3..750849d 100644 --- a/src/async_io/client.rs +++ b/src/async_io/client.rs @@ -155,13 +155,13 @@ where } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) - && resp_nonce != nonce - { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { + if resp_nonce != nonce { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); + } } Ok(()) @@ -219,13 +219,13 @@ where } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) - && resp_nonce != nonce - { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { + if resp_nonce != nonce { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); + } } Ok(response) diff --git a/src/sync/client.rs b/src/sync/client.rs index c7fb3e6..451acb3 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -238,13 +238,13 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) - && resp_nonce != nonce - { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { + if resp_nonce != nonce { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); + } } Ok(()) @@ -302,13 +302,13 @@ impl DiscordIpcClient { } // Verify nonce matches to ensure we got the right response - if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) - && resp_nonce != nonce - { - return Err(DiscordIpcError::InvalidResponse(format!( - "Nonce mismatch: expected {}, got {}", - nonce, resp_nonce - ))); + if let Some(resp_nonce) = response.get("nonce").and_then(|n| n.as_str()) { + if resp_nonce != nonce { + return Err(DiscordIpcError::InvalidResponse(format!( + "Nonce mismatch: expected {}, got {}", + nonce, resp_nonce + ))); + } } Ok(response) From 18efbcaa47889e169718a16b17fac7293513b50c Mon Sep 17 00:00:00 2001 From: Sreehari Anil Date: Sun, 19 Oct 2025 21:59:14 +0530 Subject: [PATCH 62/62] dev: added allow collapasible if to conserve MSRV --- src/activity/types.rs | 2 ++ src/async_io/client.rs | 2 ++ src/sync/client.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/activity/types.rs b/src/activity/types.rs index 173fa12..6be7dda 100644 --- a/src/activity/types.rs +++ b/src/activity/types.rs @@ -1,3 +1,5 @@ +#![allow(clippy::collapsible_if)] + use serde::{Deserialize, Serialize}; /// Rich Presence Activity diff --git a/src/async_io/client.rs b/src/async_io/client.rs index 750849d..0fd99bd 100644 --- a/src/async_io/client.rs +++ b/src/async_io/client.rs @@ -1,5 +1,7 @@ //! Async Discord IPC Client implementation +#![allow(clippy::collapsible_if)] + use bytes::{BufMut, BytesMut}; use serde_json::{Value, json}; use std::collections::VecDeque; diff --git a/src/sync/client.rs b/src/sync/client.rs index 451acb3..dbcc8fb 100644 --- a/src/sync/client.rs +++ b/src/sync/client.rs @@ -1,3 +1,5 @@ +#![allow(clippy::collapsible_if)] + use serde_json::{Value, json}; use std::collections::VecDeque; use std::process;