unix-systems support

This commit is contained in:
AmokDev 2025-06-07 23:02:22 +03:00
parent 4a7ab5082f
commit bb2f8ccaa6
8 changed files with 211 additions and 70 deletions

View file

@ -16,8 +16,13 @@ byteorder = "1.5.0"
serde_json = "1.0.108" serde_json = "1.0.108"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
log = "0.4.20" log = "0.4.20"
[target.'cfg(windows)'.dependencies]
named_pipe = "0.4.1" named_pipe = "0.4.1"
[target.'cfg(unix)'.dependencies]
[dependencies.uuid] [dependencies.uuid]
version = "1.6.1" version = "1.6.1"
features = ["v4"] features = ["v4"]

View file

@ -1,5 +1,5 @@
# Rust Discord Activity # 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) [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: This sets-up a new Activity for the current Discord user:
<img alt="Discord Rich Presence" src="https://imgur.com/gf9pOen.png" width="300"/> <img alt="Discord Rich Presence" src="https://imgur.com/gf9pOen.png" width="300"/>
## Limits
For the moment, the library only works with WINDOWS (suck UNIXoids 🖕) and local Discord application.

View file

@ -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;

132
src/client/unix_ipc.rs Normal file
View file

@ -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<UnixStream>,
}
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<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
let payload = payload.to_string();
let mut data: Vec<u8> = Vec::new();
data.write_u32::<LittleEndian>(opcode as u32)?;
data.write_u32::<LittleEndian>(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<dyn Error>> {
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::<Value>(&payload)?;
debug!("{:?}", json_data);
Ok((op, json_data))
}
fn extract_payload(&mut self, mut data: &[u8]) -> Result<(u32, String), Box<dyn Error>> {
let opcode = data.read_u32::<LittleEndian>()?;
let payload_len = data.read_u32::<LittleEndian>()? as usize;
let mut payload = String::with_capacity(payload_len);
data.read_to_string(&mut payload)?;
Ok((opcode, payload))
}
}

View file

@ -12,13 +12,8 @@ use crate::models::error::Error::DiscordNotFound;
/// Client used to communicate with Discord through IPC. /// Client used to communicate with Discord through IPC.
pub struct DiscordClient { pub struct DiscordClient {
/// ID of Discord Application, see <https://discord.com/developers> for more info
pub id: String, pub id: String,
/// Boolean stating if Client is connected to Discord App.
pub is_connected: bool, pub is_connected: bool,
/// Unix Stream socket of Client Connection.
socket: Option<PipeClient>, socket: Option<PipeClient>,
} }

View file

@ -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("<application_id>");
//!
//! 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:
//!
//! <img alt="Discord Rich Presence" src="https://imgur.com/gf9pOen.png" width="300"/>
//!
//! ## 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 client;
mod models; mod models;
mod tests;
pub use client::ipc::DiscordClient; pub use client::DiscordClient;
pub use models::activity::Activity; pub use models::activity::Activity;
pub use models::activity_data::{ pub use models::activity_data::{
activity_flag::ActivityFlag, activity_type::ActivityType, asset::Asset, button::Button, activity_flag::ActivityFlag, activity_type::ActivityType, asset::Asset, button::Button,

1
src/tests/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod rpc;

61
src/tests/rpc.rs Normal file
View file

@ -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);
}
}