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:
-
-## 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:
-//!
-//!
-//!
-//! ## 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