Compare commits
No commits in common. "136d617e6fe33cae5460006286c8ecf8dad9ccd6" and "4a7ab5082ffec7b4ec2ecd0d84c18bc1d62b7d09" have entirely different histories.
136d617e6f
...
4a7ab5082f
8 changed files with 70 additions and 208 deletions
|
|
@ -16,13 +16,8 @@ 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 for Windows, Linux and MacOS_
|
_A lightweight Rust library to control Discord Rich Presence_
|
||||||
|
|
||||||
[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,3 +52,6 @@ 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.
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,13 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,9 +1 @@
|
||||||
#[cfg(windows)]
|
pub mod ipc;
|
||||||
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;
|
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
src/lib.rs
62
src/lib.rs
|
|
@ -1,8 +1,66 @@
|
||||||
|
//! # 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::DiscordClient;
|
pub use client::ipc::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 +0,0 @@
|
||||||
pub mod rpc;
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
|
|
||||||
#[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