diff --git a/src/client.rs b/src/client.rs index 69a341e..8552dd1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,21 @@ -use connection::{Connection, SocketConnection}; -use models::{Handshake, OpCode}; +use serde::{Serialize, de::DeserializeOwned}; + +use connection::{ + Connection, + SocketConnection, +}; +use models::{ + OpCode, + Command, + Event, + payload::Payload, + commands::{SubscriptionArgs, Subscription}, +}; + #[cfg(feature = "rich_presence")] -use rich_presence::{SetActivityArgs, SetActivity}; -use error::Result; +use models::rich_presence::{SetActivityArgs, Activity}; +use error::{Result, Error}; +use utils; pub struct Client @@ -10,14 +23,14 @@ pub struct Client { client_id: u64, version: u32, - socket: T, + connection: T, } impl Client where T: Connection { - pub fn with_connection(client_id: u64, socket: T) -> Result { - Ok(Self { version: 1, client_id, socket }) + pub fn with_connection(client_id: u64, connection: T) -> Result { + Ok(Self { version: 1, client_id, connection }) } pub fn start(mut self) -> Result { @@ -25,13 +38,30 @@ impl Client Ok(self) } - #[cfg(feature = "rich_presence")] - pub fn set_activity(&mut self, f: F) -> Result<()> - where F: FnOnce(SetActivity) -> SetActivity + pub fn execute(&mut self, cmd: Command, args: A, evt: Option) -> Result> + where A: Serialize, + E: Serialize + DeserializeOwned { - let args = SetActivityArgs::command(f(SetActivity::new())); - self.socket.send(OpCode::Frame, args)?; - Ok(()) + self.connection.send(OpCode::Frame, Payload::with_nonce(cmd, Some(args), None, evt))?; + let response: Payload = self.connection.recv()?.into(); + + match response.evt { + Some(Event::Error) => Err(Error::SubscriptionFailed), + _ => Ok(response) + } + } + + #[cfg(feature = "rich_presence")] + pub fn set_activity(&mut self, f: F) -> Result> + where F: FnOnce(Activity) -> Activity + { + self.execute(Command::SetActivity, SetActivityArgs::new(f), None) + } + + pub fn subscribe(&mut self, evt: Event, f: F) -> Result> + where F: FnOnce(SubscriptionArgs) -> SubscriptionArgs + { + self.execute(Command::Subscribe, f(SubscriptionArgs::new()), Some(evt)) } // private @@ -39,7 +69,13 @@ impl Client fn handshake(&mut self) -> Result<()> { let client_id = self.client_id; let version = self.version; - self.socket.send(OpCode::Handshake, Handshake::new(client_id, version))?; + let hs = json![{ + "client_id": client_id.to_string(), + "v": version, + "nonce": utils::nonce() + }]; + self.connection.send(OpCode::Handshake, hs)?; + self.connection.recv()?; Ok(()) } } diff --git a/src/connection/base.rs b/src/connection/base.rs index 186392a..8643731 100644 --- a/src/connection/base.rs +++ b/src/connection/base.rs @@ -1,11 +1,12 @@ use std::{ io::{Write, Read}, marker::Sized, - fmt::Debug, path::PathBuf, }; -use models::{Payload, Message, OpCode}; +use serde::Serialize; + +use models::message::{Message, OpCode}; use error::Result; @@ -26,25 +27,25 @@ pub trait Connection } fn send(&mut self, opcode: OpCode, payload: T) -> Result<()> - where T: Payload + Debug + where T: Serialize { - debug!("payload: {:#?}", payload); - match Message::new(opcode, payload).encode() { + let message = Message::new(opcode, payload); + debug!("{:?}", message); + match message.encode() { Err(why) => error!("{:?}", why), Ok(bytes) => { self.socket().write_all(bytes.as_ref())?; - debug!("sent opcode: {:?}", opcode); - self.recv()?; } }; Ok(()) } - fn recv(&mut self) -> Result> { + fn recv(&mut self) -> Result { let mut buf: Vec = vec![0; 1024]; let n = self.socket().read(buf.as_mut_slice())?; buf.resize(n, 0); - debug!("{:?}", Message::decode(&buf)); - Ok(buf) + let message = Message::decode(&buf)?; + debug!("{:?}", message); + Ok(message) } } diff --git a/src/error.rs b/src/error.rs index de09009..915288d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,7 @@ use std::{ pub enum Error { Io(IoError), Conversion, + SubscriptionFailed, } impl Display for Error { @@ -26,6 +27,7 @@ impl StdError for Error { fn description(&self) -> &str { match *self { Error::Conversion => "Failed to convert values", + Error::SubscriptionFailed => "Failed to subscribe to event", Error::Io(ref err) => err.description() } } diff --git a/src/lib.rs b/src/lib.rs index 56e123f..385ba6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ extern crate log; #[macro_use] extern crate serde_derive; extern crate serde; +#[macro_use] extern crate serde_json; extern crate byteorder; extern crate uuid; @@ -15,12 +16,8 @@ mod macros; mod error; mod utils; mod connection; -mod models; -mod rich_presence; - +pub mod models; pub mod client; pub use client::Client; -#[cfg(feature = "rich_presence")] -pub use rich_presence::*; pub use connection::{Connection, SocketConnection}; diff --git a/src/macros.rs b/src/macros.rs index 31fefe4..9a78bca 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,4 +1,4 @@ -macro_rules! message_func { +macro_rules! builder_func { [ $name:ident, $type:tt func ] => { pub fn $name(mut self, func: F) -> Self where F: FnOnce($type) -> $type @@ -22,9 +22,9 @@ macro_rules! message_func { }; } -macro_rules! message_format { +macro_rules! builder { [ @st ( $name:ident $field:tt: $type:tt alias = $alias:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @st + builder![ @st ( $name $($rest)* ) -> ( $($out)* #[serde(skip_serializing_if = "Option::is_none", rename = $alias)] @@ -34,11 +34,11 @@ macro_rules! message_format { }; [ @st ( $name:ident $field:tt: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ]; + builder![ @st ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ]; }; [ @st ( $name:ident $field:ident: $type:ty, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @st + builder![ @st ( $name $($rest)* ) -> ( $($out)* #[serde(skip_serializing_if = "Option::is_none")] @@ -48,20 +48,20 @@ macro_rules! message_format { }; [ @st ( $name:ident ) -> ( $($out:tt)* ) ] => { - #[derive(Debug, Default, Serialize)] + #[derive(Debug, Default, PartialEq, Deserialize, Serialize)] pub struct $name { $($out)* } }; [ @im ( $name:ident $field:ident: $type:tt func, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @im ( $name $($rest)* ) -> ( message_func![$field, $type func]; $($out)* ) ]; + builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type func]; $($out)* ) ]; }; [ @im ( $name:ident $field:ident: $type:tt alias = $modifier:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @im ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ]; + builder![ @im ( $name $field: $type, $($rest)* ) -> ( $($out)* ) ]; }; [ @im ( $name:ident $field:ident: $type:tt, $($rest:tt)* ) -> ( $($out:tt)* ) ] => { - message_format![ @im ( $name $($rest)* ) -> ( message_func![$field, $type]; $($out)* ) ]; + builder![ @im ( $name $($rest)* ) -> ( builder_func![$field, $type]; $($out)* ) ]; }; [ @im ( $name:ident ) -> ( $($out:tt)* ) ] => { @@ -75,7 +75,7 @@ macro_rules! message_format { }; [ $name:ident $($body:tt)* ] => { - message_format![@st ( $name $($body)* ) -> () ]; - message_format![@im ( $name $($body)* ) -> () ]; + builder![@st ( $name $($body)* ) -> () ]; + builder![@im ( $name $($body)* ) -> () ]; } } diff --git a/src/models/command.rs b/src/models/command.rs deleted file mode 100644 index e50f165..0000000 --- a/src/models/command.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::Serialize; - -use super::Payload; -use utils::nonce; - - -#[derive(Debug, Default, Serialize)] -pub struct Command - where T: Serialize -{ - pub nonce: String, - pub cmd: String, - pub args: T, -} - -impl Command - where T: Serialize -{ - pub fn new(cmd: S, args: T) -> Self - where S: Into - { - Command { - cmd: cmd.into(), - nonce: nonce(), - args: args - } - } -} - -impl Payload for Command - where T: Serialize {} diff --git a/src/models/commands.rs b/src/models/commands.rs new file mode 100644 index 0000000..7e0a49e --- /dev/null +++ b/src/models/commands.rs @@ -0,0 +1,11 @@ +use super::shared::PartialUser; + + +builder!{SubscriptionArgs + secret: String, // Activity{Join,Spectate} + user: PartialUser, // ActivityJoinRequest +} + +builder!{Subscription + evt: String, +} diff --git a/src/models/events.rs b/src/models/events.rs new file mode 100644 index 0000000..d86f0e2 --- /dev/null +++ b/src/models/events.rs @@ -0,0 +1,19 @@ +use super::shared::PartialUser; + + +builder!{ReadyEvent + v: u32, + config: RpcServerConfiguration, + user: PartialUser, +} + +builder!{ErrorEvent + code: u32, + message: String, +} + +builder!{RpcServerConfiguration + cdn_host: String, + api_endpoint: String, + environment: String, +} diff --git a/src/models/handshake.rs b/src/models/handshake.rs deleted file mode 100644 index 95c94bb..0000000 --- a/src/models/handshake.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::Payload; -use utils::nonce; - - -#[derive(Debug, Default, Serialize)] -pub struct Handshake { - nonce: String, - v: u32, - client_id: String, -} - -impl Handshake { - pub fn new(client_id: u64, version: u32) -> Self { - Self { - nonce: nonce(), - v: version, - client_id: client_id.to_string() - } - } -} - -impl Payload for Handshake {} diff --git a/src/models/message.rs b/src/models/message.rs index a2a3dcf..9bb93b5 100644 --- a/src/models/message.rs +++ b/src/models/message.rs @@ -30,40 +30,42 @@ impl OpCode { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Message { - opcode: OpCode, - message: String, + pub opcode: OpCode, + pub payload: String, } impl Message { - pub fn new(opcode: OpCode, message: T) -> Self + pub fn new(opcode: OpCode, payload: T) -> Self where T: Serialize { - Message { - opcode: opcode, - message: serde_json::to_string(&message).unwrap() - } + Self { opcode, payload: serde_json::to_string(&payload).unwrap() } } pub fn encode(&self) -> Result> { let mut bytes: Vec = vec![]; + bytes.write_u32::(self.opcode as u32)?; - bytes.write_u32::(self.message.len() as u32)?; - write!(bytes, "{}", self.message)?; + bytes.write_u32::(self.payload.len() as u32)?; + write!(bytes, "{}", self.payload)?; + Ok(bytes) } pub fn decode(bytes: &[u8]) -> Result { let mut reader = io::Cursor::new(bytes); - let mut message = String::new(); + let mut payload = String::new(); + let opcode = OpCode::try_from(reader.read_u32::()?)?; reader.read_u32::()?; - reader.read_to_string(&mut message)?; - Ok(Message { opcode, message }) + reader.read_to_string(&mut payload)?; + + Ok(Self { opcode, payload }) } } + #[cfg(test)] mod tests { use super::*; diff --git a/src/models/mod.rs b/src/models/mod.rs index c87bbf1..9e723ef 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,11 +1,57 @@ -mod message; -mod command; -mod handshake; +mod shared; +pub mod message; +pub mod payload; +pub mod commands; +pub mod events; +pub mod rich_presence; -use serde::Serialize; + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Command { + Dispatch, + Authorize, + Subscribe, + Unsubscribe, + #[cfg(feature = "rich_presence")] + SetActivity, +} + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Event { + Ready, + Error, + #[cfg(feature = "rich_presence")] + ActivityJoin, + #[cfg(feature = "rich_presence")] + ActivitySpectate, + #[cfg(feature = "rich_presence")] + ActivityJoinRequest, +} pub use self::message::{Message, OpCode}; -pub use self::command::Command; -pub use self::handshake::Handshake; +pub use self::commands::*; +pub use self::events::*; -pub trait Payload: Serialize {} +#[cfg(feature = "rich_presence")] +pub use self::rich_presence::*; + +pub mod prelude { + pub use super::Command; + pub use super::Event; + #[cfg(feature = "rich_presence")] + pub use super::rich_presence::{ + SetActivityArgs, + ActivityJoinEvent, + ActivitySpectateEvent, + ActivityJoinRequestEvent + }; + pub use super::commands::{ + SubscriptionArgs, Subscription + }; + pub use super::events::{ + ReadyEvent, + ErrorEvent, + }; +} diff --git a/src/models/payload.rs b/src/models/payload.rs new file mode 100644 index 0000000..3e604fa --- /dev/null +++ b/src/models/payload.rs @@ -0,0 +1,45 @@ +use std::{ + convert::From, +}; + +use serde::{Serialize, de::DeserializeOwned}; +use serde_json; + +use super::{Command, Event, Message}; +use utils; + + +#[derive(Debug, PartialEq, Deserialize, Serialize)] +pub struct Payload + where T: Serialize +{ + pub cmd: Command, + + #[serde(skip_serializing_if = "Option::is_none")] + pub args: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub evt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, +} + +impl Payload + where T: Serialize +{ + pub fn with_nonce(cmd: Command, args: Option, data: Option, evt: Option) -> Self { + Self { cmd, args, data, evt, nonce: Some(utils::nonce()) } + } +} + +impl From for Payload + where T: Serialize + DeserializeOwned +{ + fn from(message: Message) -> Self { + serde_json::from_str(&message.payload).unwrap() + } +} diff --git a/src/rich_presence/set_activity.rs b/src/models/rich_presence.rs similarity index 69% rename from src/rich_presence/set_activity.rs rename to src/models/rich_presence.rs index d027b63..28e9b78 100644 --- a/src/rich_presence/set_activity.rs +++ b/src/models/rich_presence.rs @@ -1,54 +1,69 @@ -use models::Command; -use utils::pid; +#![cfg(feature = "rich_presence")] + +use super::shared::PartialUser; +use utils; -#[derive(Debug, Default, Serialize)] +#[derive(Debug, PartialEq, Deserialize, Serialize)] pub struct SetActivityArgs { pid: i32, - activity: SetActivity, + activity: Activity, } impl SetActivityArgs { - pub fn command(args: SetActivity) -> Command { - Command::new("SET_ACTIVITY", Self { - pid: pid(), - activity: args - }) + pub fn new(f: F) -> Self + where F: FnOnce(Activity) -> Activity + { + Self { pid: utils::pid(), activity: f(Activity::new()) } } } -message_format![SetActivity - state: String, - details: String, - instance: bool, - timestamps: SetActivityTimestamps func, - assets: SetActivityAssets func, - party: SetActivityParty func, - secrets: SetActivitySecrets func, -]; +builder!{ActivityJoinEvent + secret: String, +} -message_format![SetActivityTimestamps +builder!{ActivitySpectateEvent + secret: String, +} + +builder!{ActivityJoinRequestEvent + user: PartialUser, +} + + +builder!{Activity + state: String, + details: String, + instance: bool, + timestamps: ActivityTimestamps func, + assets: ActivityAssets func, + party: ActivityParty func, + secrets: ActivitySecrets func, +} + +builder!{ActivityTimestamps start: u32, end: u32, -]; +} -message_format![SetActivityAssets +builder!{ActivityAssets large_image: String, large_text: String, small_image: String, small_text: String, -]; +} -message_format![SetActivityParty +builder!{ActivityParty id: u32, size: (u32, u32), -]; +} -message_format![SetActivitySecrets +builder!{ActivitySecrets join: String, spectate: String, game: String alias = "match", -]; +} + #[cfg(test)] mod tests { @@ -86,7 +101,7 @@ r###"{ #[test] fn test_serialize_full_activity() { - let activity = SetActivity::new() + let activity = Activity::new() .state("rusting") .details("detailed") .instance(true) @@ -113,7 +128,7 @@ r###"{ #[test] fn test_serialize_empty_activity() { - let activity = SetActivity::new(); + let activity = Activity::new(); let json = serde_json::to_string(&activity).unwrap(); assert_eq![json, "{}"]; } diff --git a/src/models/shared.rs b/src/models/shared.rs new file mode 100644 index 0000000..8fd8b89 --- /dev/null +++ b/src/models/shared.rs @@ -0,0 +1,6 @@ +builder!{PartialUser + id: String, + username: String, + discriminator: String, + avatar: String, +} diff --git a/src/rich_presence/mod.rs b/src/rich_presence/mod.rs deleted file mode 100644 index 3452b11..0000000 --- a/src/rich_presence/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg(feature = "rich_presence")] - -mod set_activity; - -pub use self::set_activity::*;