unix-systems support
This commit is contained in:
parent
4a7ab5082f
commit
bb2f8ccaa6
8 changed files with 211 additions and 70 deletions
|
|
@ -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"]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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
132
src/client/unix_ipc.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
62
src/lib.rs
62
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("<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
1
src/tests/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod rpc;
|
||||||
61
src/tests/rpc.rs
Normal file
61
src/tests/rpc.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue