From bb2f8ccaa6846ab68f9c9f2d50c91afd08fa98b5 Mon Sep 17 00:00:00 2001 From: AmokDev Date: Sat, 7 Jun 2025 23:02:22 +0300 Subject: [PATCH 1/4] unix-systems support --- Cargo.toml | 5 ++ README.md | 5 +- src/client/mod.rs | 10 ++- src/client/unix_ipc.rs | 132 ++++++++++++++++++++++++++++++ src/client/{ipc.rs => win_ipc.rs} | 5 -- src/lib.rs | 62 +------------- src/tests/mod.rs | 1 + src/tests/rpc.rs | 61 ++++++++++++++ 8 files changed, 211 insertions(+), 70 deletions(-) create mode 100644 src/client/unix_ipc.rs rename src/client/{ipc.rs => win_ipc.rs} (94%) create mode 100644 src/tests/mod.rs create mode 100644 src/tests/rpc.rs diff --git a/Cargo.toml b/Cargo.toml index 7e40b49..e79f6d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,13 @@ byteorder = "1.5.0" serde_json = "1.0.108" serde = { version = "1.0.193", features = ["derive"] } log = "0.4.20" + +[target.'cfg(windows)'.dependencies] named_pipe = "0.4.1" +[target.'cfg(unix)'.dependencies] + + [dependencies.uuid] version = "1.6.1" features = ["v4"] diff --git a/README.md b/README.md index 0e1cc0b..5eaad7c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Rust Discord Activity -_A lightweight Rust library to control Discord Rich Presence_ +_A lightweight Rust library to control Discord Rich Presence for Windows, Linux and MacOS_ [Author of the idea and 80% code](https://github.com/DylanCa/rust-discord-activity) @@ -52,6 +52,3 @@ let _ = client.send_payload(payload); This sets-up a new Activity for the current Discord user: Discord Rich Presence - -## Limits -For the moment, the library only works with WINDOWS (suck UNIXoids đź–•) and local Discord application. diff --git a/src/client/mod.rs b/src/client/mod.rs index ce14ad3..94fbfb3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1 +1,9 @@ -pub mod ipc; +#[cfg(windows)] +pub mod win_ipc; +#[cfg(unix)] +pub mod unix_ipc; + +#[cfg(windows)] +pub use win_ipc::DiscordClient as DiscordClient; +#[cfg(unix)] +pub use unix_ipc::DiscordClient as DiscordClient; diff --git a/src/client/unix_ipc.rs b/src/client/unix_ipc.rs new file mode 100644 index 0000000..04dd57d --- /dev/null +++ b/src/client/unix_ipc.rs @@ -0,0 +1,132 @@ +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use log::debug; +use serde_json::{json, Value}; +use std::error::Error; +use std::io::{Read, Write}; +use std::{ + env, + net::Shutdown, + os::unix::net::UnixStream, + path::PathBuf, + time +}; +use std::path::PathBuf; + +use crate::models::client::{commands::Commands, payload::OpCode, payload::Payload}; +use crate::models::error::Error as ErrorMsg; +use crate::models::error::Error::DiscordNotFound; + +/// Client used to communicate with Discord through IPC. +pub struct DiscordClient { + pub id: String, + pub is_connected: bool, + socket: Option, +} + +impl DiscordClient { + /// Used to instantiate a new Discord Client. + pub fn new(id: &str) -> Self { + Self { + id: id.to_string(), + is_connected: false, + socket: None, + } + } + + /// Tries to enable a connection to the Discord Application. + pub fn connect(&mut self) -> Result<(), ErrorMsg> { + let path = self.fetch_process_pathbuf().join("discord-ipc-0"); + + match UnixStream::connect(&path) { + Ok(socket) => { + self.socket.set_nonblocking(true)?; + self.socket.set_write_timeout(Some(time::Duration::from_secs(30)))?; + self.socket.set_read_timeout(Some(time::Duration::from_secs(30)))?; + self.socket = Some(socket); + self.handshake().expect("Could not handshake."); + self.is_connected = true; + Ok(()) + } + Err(_) => { + self.is_connected = false; + Err(DiscordNotFound) + } + } + } + + pub fn send_payload(&mut self, payload: Payload) -> Result<(u32, Value), Box> { + let payload = json!({ + "cmd": Commands::SetActivity.as_string(), + "args": { + "pid": std::process::id(), + payload.event_name: payload.event_data, + }, + "nonce": uuid::Uuid::new_v4().to_string(), + }); + + match self.send(payload, OpCode::MESSAGE as u8) { + Ok(retval) => { + Ok(retval) + }, + Err(e) => { + self.is_connected = false; + Err(e) + }, + } + } + + fn socket(&mut self) -> &mut PipeClient { + match &mut self.socket { + Some(socket) => socket, + None => panic!("Socket is not initialized"), + } + } + + fn fetch_process_pathbuf(&mut self) -> PathBuf { + let tmp = env::var("XDG_RUNTIME_DIR") + .or_else(|_| env::var("TMPDIR")) + .or_else(|_| env::var("TMP")) + .or_else(|_| env::var("TEMP")) + .unwrap_or_else(|_| "/tmp".to_owned()); + PathBuf::from(tmp) + } + + fn handshake(&mut self) -> Result<(u32, Value), Box> { + let payload = json!({ "v": 1, "client_id": self.id}); + + Ok(self.send(payload, OpCode::HANDSHAKE as u8)?) + } + + fn send(&mut self, payload: Value, opcode: u8) -> Result<(u32, Value), Box> { + let payload = payload.to_string(); + let mut data: Vec = Vec::new(); + + data.write_u32::(opcode as u32)?; + data.write_u32::(payload.len() as u32)?; + data.write_all(payload.as_bytes())?; + + self.socket().write_all(&data)?; + Ok(self.recv()?) + } + + fn recv(&mut self) -> Result<(u32, Value), Box> { + let mut buf = [0; 2048]; + + let byte_count = self.socket().read(&mut buf)?; + let (op, payload) = self.extract_payload(&buf[..byte_count])?; + let json_data = serde_json::from_str::(&payload)?; + + debug!("{:?}", json_data); + + Ok((op, json_data)) + } + + fn extract_payload(&mut self, mut data: &[u8]) -> Result<(u32, String), Box> { + let opcode = data.read_u32::()?; + let payload_len = data.read_u32::()? as usize; + let mut payload = String::with_capacity(payload_len); + data.read_to_string(&mut payload)?; + + Ok((opcode, payload)) + } +} diff --git a/src/client/ipc.rs b/src/client/win_ipc.rs similarity index 94% rename from src/client/ipc.rs rename to src/client/win_ipc.rs index 4fc75ef..a22b05d 100644 --- a/src/client/ipc.rs +++ b/src/client/win_ipc.rs @@ -12,13 +12,8 @@ use crate::models::error::Error::DiscordNotFound; /// Client used to communicate with Discord through IPC. pub struct DiscordClient { - /// ID of Discord Application, see for more info pub id: String, - - /// Boolean stating if Client is connected to Discord App. pub is_connected: bool, - - /// Unix Stream socket of Client Connection. socket: Option, } diff --git a/src/lib.rs b/src/lib.rs index 93da85b..eaf02ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,66 +1,8 @@ -//! # Rust Discord Activity -//! _A lightweight Rust library to control Discord Rich Presence_ -//! -//! ## Installation -//! Rust Discord Activity is available directly on [crates.io](https://crates.io/crates/rust-discord-activity): -//! `cargo add rust-discord-activity` -//! -//! ## How to use -//! 1. Instantiate a new DiscordClient -//! 2. Create your Activity and set desired data using provided structs -//! 3. Create a new Payload with your Activity -//! 4. Send your Payload through the DiscordClient -//! -//! Et voilĂ  ! -//! -//! ## Example -//! ```rust -//! let mut client = DiscordClient::new(""); -//! -//! let limg = Some(String::from("https://placehold.co/600x400/png")); -//! let simg = Some(String::from("https://placehold.co/200x100/png")); -//! let asset = Asset::new(limg, None, simg, None); -//! let now_in_millis = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); -//! let timestamp = Timestamp::new(Some(now_in_millis - 10000), None); -//! -//! let party = Party::new(None, Some((2, 4))); -//! let mut button_vec = vec![]; -//! button_vec.push(Button::new("First Button".into(), "https://google.com".into())); -//! button_vec.push(Button::new("Second Button".into(), "https://yahoo.com".into())); -//! -//! let mut activity = Activity::new(); -//! -//! activity -//! .set_state(Some("This is State".into())) -//! .set_activity_type(Some(ActivityType::LISTENING)) -//! .set_details(Some("This is Details".parse().unwrap())) -//! .set_timestamps(Some(timestamp)) -//! .set_assets(Some(asset)) -//! .set_party(Some(party)) -//! .set_instance(Some(true)) -//! .set_buttons(Some(button_vec)); -//! -//! let payload = Payload::new(EventName::Activity, EventData::Activity(activity)); -//! -//! let _ = client.send_payload(payload); -//! -//! ``` -//! -//! And voilĂ ! This sets-up a new Activity for the current Discord user: -//! -//! Discord Rich Presence -//! -//! ## Limitations -//! For the moment, the library only works with MacOS and local Discord application. -//! -//! ## Next Steps -//! - Write proper documentation for this library -//! - Write unit tests - mod client; mod models; +mod tests; -pub use client::ipc::DiscordClient; +pub use client::DiscordClient; pub use models::activity::Activity; pub use models::activity_data::{ activity_flag::ActivityFlag, activity_type::ActivityType, asset::Asset, button::Button, diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..25849ae --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod rpc; \ No newline at end of file diff --git a/src/tests/rpc.rs b/src/tests/rpc.rs new file mode 100644 index 0000000..03ecfae --- /dev/null +++ b/src/tests/rpc.rs @@ -0,0 +1,61 @@ + +#[cfg(test)] +pub mod activity_tests { + use crate::{ + DiscordClient, + Asset, + Timestamp, + Activity, + ActivityType, + Payload, + EventName, + EventData, + Party, + Button + }; + use std::time::{SystemTime, UNIX_EPOCH, Duration}; + use std::{thread}; + + #[test] + pub fn test_rpc() { + let mut client = DiscordClient::new("1380086659034648608"); + let _ = client.connect(); + let dur = Duration::from_secs(10); + + for activity_type in [ + ActivityType::LISTENING, + ActivityType::GAME, + ActivityType::COMPETING, + ActivityType::WATCHING + ] { + let limg = Some(String::from("https://placehold.co/600x400/png")); + let simg = Some(String::from("https://placehold.co/200x100/png")); + let asset = Asset::new(limg, None, simg, None); + let now_in_millis = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); + let timestamp = Timestamp::new(Some(now_in_millis - 10000), None); + + let party = Party::new(None, Some((2, 4))); + let mut button_vec = vec![]; + button_vec.push(Button::new("First Button".into(), "https://google.com".into())); + button_vec.push(Button::new("Second Button".into(), "https://yahoo.com".into())); + + let mut activity = Activity::new(); + + activity + .set_state(Some("This is State".into())) + .set_activity_type(Some(activity_type)) + .set_details(Some("This is Details".parse().unwrap())) + .set_timestamps(Some(timestamp)) + .set_assets(Some(asset)) + .set_party(Some(party)) + .set_instance(Some(true)) + .set_buttons(Some(button_vec)); + + let payload = Payload::new(EventName::Activity, EventData::Activity(activity)); + + let _ = client.send_payload(payload); + thread::sleep(dur); + } + assert!(true); + } +} \ No newline at end of file From 9bf2cdac42cb22e409d0c7e314b5a3d7e8e40f27 Mon Sep 17 00:00:00 2001 From: AmokDev Date: Sat, 7 Jun 2025 23:21:28 +0300 Subject: [PATCH 2/4] unix-systems support --- src/client/unix_ipc.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/unix_ipc.rs b/src/client/unix_ipc.rs index 04dd57d..503f0b0 100644 --- a/src/client/unix_ipc.rs +++ b/src/client/unix_ipc.rs @@ -39,9 +39,9 @@ impl DiscordClient { match UnixStream::connect(&path) { Ok(socket) => { - self.socket.set_nonblocking(true)?; - self.socket.set_write_timeout(Some(time::Duration::from_secs(30)))?; - self.socket.set_read_timeout(Some(time::Duration::from_secs(30)))?; + // self.socket.set_nonblocking(true)?; + // self.socket.set_write_timeout(Some(time::Duration::from_secs(30)))?; + // self.socket.set_read_timeout(Some(time::Duration::from_secs(30)))?; self.socket = Some(socket); self.handshake().expect("Could not handshake."); self.is_connected = true; From f77bc774ae5161fdcf384a3c383ea7a46a9c8576 Mon Sep 17 00:00:00 2001 From: AmokDev Date: Sat, 7 Jun 2025 23:23:27 +0300 Subject: [PATCH 3/4] unix-systems support --- src/client/unix_ipc.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/unix_ipc.rs b/src/client/unix_ipc.rs index 503f0b0..eb176f3 100644 --- a/src/client/unix_ipc.rs +++ b/src/client/unix_ipc.rs @@ -10,7 +10,6 @@ use std::{ path::PathBuf, time }; -use std::path::PathBuf; use crate::models::client::{commands::Commands, payload::OpCode, payload::Payload}; use crate::models::error::Error as ErrorMsg; @@ -75,7 +74,7 @@ impl DiscordClient { } } - fn socket(&mut self) -> &mut PipeClient { + fn socket(&mut self) -> &mut UnixStream { match &mut self.socket { Some(socket) => socket, None => panic!("Socket is not initialized"), From 4bff5059d94d58c677210466c6e5bab7f2105c81 Mon Sep 17 00:00:00 2001 From: AmokDev Date: Sat, 7 Jun 2025 23:27:16 +0300 Subject: [PATCH 4/4] unix-systems support --- src/client/unix_ipc.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/unix_ipc.rs b/src/client/unix_ipc.rs index eb176f3..c38885b 100644 --- a/src/client/unix_ipc.rs +++ b/src/client/unix_ipc.rs @@ -5,10 +5,8 @@ use std::error::Error; use std::io::{Read, Write}; use std::{ env, - net::Shutdown, os::unix::net::UnixStream, path::PathBuf, - time }; use crate::models::client::{commands::Commands, payload::OpCode, payload::Payload};