Compare commits
5 commits
4a7ab5082f
...
136d617e6f
| Author | SHA1 | Date | |
|---|---|---|---|
| 136d617e6f | |||
| 4bff5059d9 | |||
| f77bc774ae | |||
| 9bf2cdac42 | |||
| bb2f8ccaa6 |
8 changed files with 208 additions and 70 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
<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;
|
||||
|
|
|
|||
129
src/client/unix_ipc.rs
Normal file
129
src/client/unix_ipc.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
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,
|
||||
os::unix::net::UnixStream,
|
||||
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 UnixStream {
|
||||
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.
|
||||
pub struct DiscordClient {
|
||||
/// ID of Discord Application, see <https://discord.com/developers> 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<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 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,
|
||||
|
|
|
|||
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