diff --git a/Cargo.toml b/Cargo.toml index d7575a0..bb6fb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "rust-discord-activity" description = "A lightweight library to control Discord Rich Presence" -version = "0.1.1" +version = "0.2.0" edition = "2021" -authors = ["DylanCa "); + let limg = Some(String::from("https://placehold.co/600x400/png")); -let simg = 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); @@ -40,7 +42,6 @@ activity let payload = Payload::new(EventName::Activity, EventData::Activity(activity)); -let mut client = DiscordClient::new(""); let _ = client.send_payload(payload); ``` @@ -49,6 +50,9 @@ And voilà! This sets-up a new Activity for the current Discord user: Discord Rich Presence +## 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 diff --git a/src/client/ipc.rs b/src/client/ipc.rs index b6730a2..da53bdb 100644 --- a/src/client/ipc.rs +++ b/src/client/ipc.rs @@ -8,23 +8,37 @@ use std::os::unix::net::UnixStream; 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 { + /// ID of Discord Application, see for more info pub id: String, socket: Option, } impl DiscordClient { + /// Used to instantiate a new Discord Client. pub fn new(id: &str) -> Self { - let mut client = Self { + Self { id: id.to_string(), socket: None, - }; + } + } - client - .connect() - .expect("Could not connect to client. Is Discord running ?"); - client + /// 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 = Some(socket); + self.handshake().expect("Could not handshake."); + Ok(()) + } + Err(_) => Err(DiscordNotFound), + } } pub fn send_payload(&mut self, payload: Payload) -> Result<(u32, Value), Box> { @@ -44,21 +58,6 @@ impl DiscordClient { self.socket.as_mut().unwrap() } - fn connect(&mut self) -> Result<(), Box> { - let path = self.fetch_process_pathbuf().join("discord-ipc-0"); - - match UnixStream::connect(&path) { - Ok(socket) => { - self.socket = Some(socket); - - self.handshake().expect("Could not handshake."); - } - Err(_) => panic!("Could not connect to client. Is Discord running ?"), - } - - Ok(()) - } - fn fetch_process_pathbuf(&mut self) -> PathBuf { let mut path = String::new(); diff --git a/src/lib.rs b/src/lib.rs index 3060a76..93da85b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,64 @@ -pub mod client; -pub mod models; +//! # 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: +//! +//! Discord Rich Presence +//! +//! ## 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; pub use client::ipc::DiscordClient; pub use models::activity::Activity; diff --git a/src/models/activity.rs b/src/models/activity.rs index ad36ceb..f40f39c 100644 --- a/src/models/activity.rs +++ b/src/models/activity.rs @@ -4,57 +4,73 @@ use crate::models::activity_data::{ }; use serde::Serialize; -/// Test Doc +/// Represents a Discord Activity object to be send to Discord application. +/// See for more information. #[derive(Serialize, Debug)] pub struct Activity { + /// Name of the Discord Application - Read Only. #[serde(skip_serializing)] name: String, + /// Type of Activity - Will be discarded by Discord App. #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "type")] activity_type: Option, + /// Livestream URL, accepts only Twitch and Youtube links - Will be discarded by Discord App. #[serde(skip_serializing_if = "Option::is_none")] url: Option, + /// Time of creation of the Activity. #[serde(skip_serializing_if = "Option::is_none")] created_at: Option, + /// Timestamps for the Activity. Used to set the "elapsed / remaining" countdown on Discord Activity. #[serde(skip_serializing_if = "Option::is_none")] timestamps: Option, + /// ID of Discord Application provided when instantiating DiscordClient. #[serde(skip_serializing_if = "Option::is_none")] application_id: Option, + /// First line of Discord Activity. #[serde(skip_serializing_if = "Option::is_none")] details: Option, + /// Second line of Discord Activity. #[serde(skip_serializing_if = "Option::is_none")] state: Option, + /// Sets a custom Emoji on the Discord Activity - Will be discarded by Discord App. #[serde(skip_serializing_if = "Option::is_none")] emoji: Option, + /// Adds a player count after the State. #[serde(skip_serializing_if = "Option::is_none")] party: Option, + /// Activity Large image and Small image. #[serde(skip_serializing_if = "Option::is_none")] assets: Option, + /// Adds a Secret URI to the activity to enable Chat Join messages. #[serde(skip_serializing_if = "Option::is_none")] secrets: Option, + /// Whether activity is in an Instance context, like an ongoing match. #[serde(skip_serializing_if = "Option::is_none")] instance: Option, #[serde(skip_serializing_if = "Option::is_none")] flags: Option, + /// List of buttons added at the bottom of the Activity. Up to 2 buttons are supported by Discord. #[serde(skip_serializing_if = "Option::is_none")] buttons: Option>, } impl Activity { + /// Instantiates a new Activity. pub fn new() -> Activity { Self { name: "".to_string(), @@ -75,11 +91,13 @@ impl Activity { } } + /// Sets a name for the Activity - Will be discarded by Discord App. pub fn set_name(&mut self, name: String) -> &mut Self { self.name = name; self } + /// Sets a new ActivityType - Will be discarded by Discord App. pub fn set_activity_type(&mut self, activity_type: Option) -> &mut Self { match activity_type { Some(val) => self.activity_type = Some(val as i8), @@ -89,61 +107,73 @@ impl Activity { self } + /// Sets a streaming URL. pub fn set_url(&mut self, url: Option) -> &mut Self { self.url = url; self } + /// Sets a created_at for the Activity. pub fn set_created_at(&mut self, created_at: Option) -> &mut Self { self.created_at = created_at; self } + /// Sets new Timestamps for the Activity. pub fn set_timestamps(&mut self, timestamps: Option) -> &mut Self { self.timestamps = timestamps; self } + /// Sets an Application ID for the Activity - Will be discarded by Discord App. pub fn set_application_id(&mut self, application_id: Option) -> &mut Self { self.application_id = application_id; self } + /// Sets the details for the current Activity. pub fn set_details(&mut self, details: Option) -> &mut Self { self.details = details; self } + /// Sets a state for the current Activity. pub fn set_state(&mut self, state: Option) -> &mut Self { self.state = state; self } + /// Sets an emoji for the current Activity. pub fn set_emoji(&mut self, emoji: Option) -> &mut Self { self.emoji = emoji; self } + /// Sets the party count for the current Activity. pub fn set_party(&mut self, party: Option) -> &mut Self { self.party = party; self } + /// Sets the Image Assets for the current Activity. pub fn set_assets(&mut self, assets: Option) -> &mut Self { self.assets = assets; self } + /// Sets a Secret for the current Activity. pub fn set_secrets(&mut self, secrets: Option) -> &mut Self { self.secrets = secrets; self } + /// Sets the instance boolean for the current Activity. pub fn set_instance(&mut self, instance: Option) -> &mut Self { self.instance = instance; self } + /// Sets the flags for the current Activity. pub fn set_flags(&mut self, flag: Option) -> &mut Self { match flag { Some(val) => self.flags = Some(val as i8), @@ -153,6 +183,7 @@ impl Activity { self } + /// Sets the Buttons for the current Activity. Up to 2 buttons are supported by Discord. pub fn set_buttons(&mut self, buttons: Option>) -> &mut Self { self.buttons = buttons; self diff --git a/src/models/activity_data/activity_flag.rs b/src/models/activity_data/activity_flag.rs index 2ff1bad..3e3840c 100644 --- a/src/models/activity_data/activity_flag.rs +++ b/src/models/activity_data/activity_flag.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// List of Activity Flags to send to Discord Client. #[derive(Serialize, Debug)] pub enum ActivityFlag { Instance = 1, diff --git a/src/models/activity_data/activity_type.rs b/src/models/activity_data/activity_type.rs index 017fbf0..7c27a75 100644 --- a/src/models/activity_data/activity_type.rs +++ b/src/models/activity_data/activity_type.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// List of Activity Type - Only Game is supported for the moment. #[derive(Serialize, Debug)] pub enum ActivityType { GAME = 0, diff --git a/src/models/activity_data/asset.rs b/src/models/activity_data/asset.rs index 9a2f937..8346b49 100644 --- a/src/models/activity_data/asset.rs +++ b/src/models/activity_data/asset.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// Contains Large and Small images of an Activity. #[derive(Serialize, Debug)] pub struct Asset { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/models/activity_data/button.rs b/src/models/activity_data/button.rs index b0bfe83..56953ee 100644 --- a/src/models/activity_data/button.rs +++ b/src/models/activity_data/button.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// Simple structure containing a label and an URL to form a Discord Activity button. #[derive(Serialize, Debug)] pub struct Button { label: String, diff --git a/src/models/activity_data/emoji.rs b/src/models/activity_data/emoji.rs index 7dca8a5..c0de580 100644 --- a/src/models/activity_data/emoji.rs +++ b/src/models/activity_data/emoji.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// Contains data of an Emoji. ID must be a Discord Emoji ID. #[derive(Serialize, Debug)] pub struct Emoji { name: String, diff --git a/src/models/activity_data/party.rs b/src/models/activity_data/party.rs index d7219d3..e63d140 100644 --- a/src/models/activity_data/party.rs +++ b/src/models/activity_data/party.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// Contains data of a Party. #[derive(Serialize, Debug)] pub struct Party { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/models/activity_data/secret.rs b/src/models/activity_data/secret.rs index ebbfafb..5cdf919 100644 --- a/src/models/activity_data/secret.rs +++ b/src/models/activity_data/secret.rs @@ -1,5 +1,6 @@ use serde::Serialize; +/// Contains Secrets URIs to join, spectate or instantiate a match through Discord Chat. #[derive(Serialize, Debug)] pub struct Secret { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/models/activity_data/timestamp.rs b/src/models/activity_data/timestamp.rs index 48f32be..336f1d1 100644 --- a/src/models/activity_data/timestamp.rs +++ b/src/models/activity_data/timestamp.rs @@ -1,5 +1,9 @@ use serde::Serialize; +/// Contains start and end time for an Activity. +/// Must be in Milliseconds since UNIX_EPOCH time. +/// If only Start is set and is in the past, it will display "xx:xx elapsed" +/// Otherwise if End is set, it will display "xx:xx remaining" #[derive(Serialize, Debug)] pub struct Timestamp { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/models/client/commands.rs b/src/models/client/commands.rs index 22e7ed0..c4b3f92 100644 --- a/src/models/client/commands.rs +++ b/src/models/client/commands.rs @@ -1,3 +1,5 @@ +/// List of Commands to send through IPC to Discord Client. +/// Currently only supports SET_ACTIVITY. pub enum Commands { SetActivity, } diff --git a/src/models/client/event.rs b/src/models/client/event.rs index 6de2592..a2f331f 100644 --- a/src/models/client/event.rs +++ b/src/models/client/event.rs @@ -2,12 +2,14 @@ use serde::Serialize; use crate::models::activity::Activity; +/// List of EventData to send to Discord - Currently only supports Activity. #[derive(Serialize, Debug)] #[serde(untagged)] pub enum EventData { Activity(Activity), } +/// List of EventName to send to Discord - Currently only supports Activity. pub enum EventName { Activity, } diff --git a/src/models/client/payload.rs b/src/models/client/payload.rs index e03c6a1..68eb047 100644 --- a/src/models/client/payload.rs +++ b/src/models/client/payload.rs @@ -1,11 +1,13 @@ use crate::models::client::event::{EventData, EventName}; use serde::Serialize; +/// List of OpCode to send to Discord App through IPC. pub enum OpCode { HANDSHAKE, MESSAGE, } +/// Payload object used to encapsulate data to send to Discord Client. #[derive(Serialize, Debug)] pub struct Payload { pub event_name: String, diff --git a/src/models/error.rs b/src/models/error.rs new file mode 100644 index 0000000..99043fb --- /dev/null +++ b/src/models/error.rs @@ -0,0 +1,20 @@ +use std::borrow::Cow; +use std::fmt; +use std::fmt::{Display, Formatter}; + +/// Custom Error list for the library. +pub enum Error { + DiscordNotFound, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let msg = match self { + Error::DiscordNotFound => { + Cow::Borrowed("Could not connect to client. Is Discord running ?") + } + }; + + f.write_str(&msg) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 443e22c..d417383 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ pub mod activity; pub mod activity_data; pub mod client; +pub mod error;