Model restructuring

* Remove old Command payload and replace with generic one.
* Move Rich Presence model back to src/models.
* Add Subscription command, Ready event and Error event models.
* Add subscribe method to client and implement simple error detection.
This commit is contained in:
Patrick Auernig 2018-04-06 02:12:59 +02:00
parent a585bb6495
commit 95d748f211
15 changed files with 268 additions and 146 deletions

View File

@ -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<T>
@ -10,14 +23,14 @@ pub struct Client<T>
{
client_id: u64,
version: u32,
socket: T,
connection: T,
}
impl<T> Client<T>
where T: Connection
{
pub fn with_connection(client_id: u64, socket: T) -> Result<Self> {
Ok(Self { version: 1, client_id, socket })
pub fn with_connection(client_id: u64, connection: T) -> Result<Self> {
Ok(Self { version: 1, client_id, connection })
}
pub fn start(mut self) -> Result<Self> {
@ -25,13 +38,30 @@ impl<T> Client<T>
Ok(self)
}
#[cfg(feature = "rich_presence")]
pub fn set_activity<F>(&mut self, f: F) -> Result<()>
where F: FnOnce(SetActivity) -> SetActivity
pub fn execute<A, E>(&mut self, cmd: Command, args: A, evt: Option<Event>) -> Result<Payload<E>>
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<E> = self.connection.recv()?.into();
match response.evt {
Some(Event::Error) => Err(Error::SubscriptionFailed),
_ => Ok(response)
}
}
#[cfg(feature = "rich_presence")]
pub fn set_activity<F>(&mut self, f: F) -> Result<Payload<Activity>>
where F: FnOnce(Activity) -> Activity
{
self.execute(Command::SetActivity, SetActivityArgs::new(f), None)
}
pub fn subscribe<F>(&mut self, evt: Event, f: F) -> Result<Payload<Subscription>>
where F: FnOnce(SubscriptionArgs) -> SubscriptionArgs
{
self.execute(Command::Subscribe, f(SubscriptionArgs::new()), Some(evt))
}
// private
@ -39,7 +69,13 @@ impl<T> Client<T>
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(())
}
}

View File

@ -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<T>(&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<Vec<u8>> {
fn recv(&mut self) -> Result<Message> {
let mut buf: Vec<u8> = 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)
}
}

View File

@ -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()
}
}

View File

@ -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};

View File

@ -1,4 +1,4 @@
macro_rules! message_func {
macro_rules! builder_func {
[ $name:ident, $type:tt func ] => {
pub fn $name<F>(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)* ) -> () ];
}
}

View File

@ -1,31 +0,0 @@
use serde::Serialize;
use super::Payload;
use utils::nonce;
#[derive(Debug, Default, Serialize)]
pub struct Command<T>
where T: Serialize
{
pub nonce: String,
pub cmd: String,
pub args: T,
}
impl<T> Command<T>
where T: Serialize
{
pub fn new<S>(cmd: S, args: T) -> Self
where S: Into<String>
{
Command {
cmd: cmd.into(),
nonce: nonce(),
args: args
}
}
}
impl<T> Payload for Command<T>
where T: Serialize {}

11
src/models/commands.rs Normal file
View File

@ -0,0 +1,11 @@
use super::shared::PartialUser;
builder!{SubscriptionArgs
secret: String, // Activity{Join,Spectate}
user: PartialUser, // ActivityJoinRequest
}
builder!{Subscription
evt: String,
}

19
src/models/events.rs Normal file
View File

@ -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,
}

View File

@ -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 {}

View File

@ -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<T>(opcode: OpCode, message: T) -> Self
pub fn new<T>(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<Vec<u8>> {
let mut bytes: Vec<u8> = vec![];
bytes.write_u32::<LittleEndian>(self.opcode as u32)?;
bytes.write_u32::<LittleEndian>(self.message.len() as u32)?;
write!(bytes, "{}", self.message)?;
bytes.write_u32::<LittleEndian>(self.payload.len() as u32)?;
write!(bytes, "{}", self.payload)?;
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
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::<LittleEndian>()?)?;
reader.read_u32::<LittleEndian>()?;
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::*;

View File

@ -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,
};
}

45
src/models/payload.rs Normal file
View File

@ -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<T>
where T: Serialize
{
pub cmd: Command,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evt: Option<Event>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
}
impl<T> Payload<T>
where T: Serialize
{
pub fn with_nonce(cmd: Command, args: Option<T>, data: Option<T>, evt: Option<Event>) -> Self {
Self { cmd, args, data, evt, nonce: Some(utils::nonce()) }
}
}
impl<T> From<Message> for Payload<T>
where T: Serialize + DeserializeOwned
{
fn from(message: Message) -> Self {
serde_json::from_str(&message.payload).unwrap()
}
}

View File

@ -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<Self> {
Command::new("SET_ACTIVITY", Self {
pid: pid(),
activity: args
})
pub fn new<F>(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, "{}"];
}

6
src/models/shared.rs Normal file
View File

@ -0,0 +1,6 @@
builder!{PartialUser
id: String,
username: String,
discriminator: String,
avatar: String,
}

View File

@ -1,5 +0,0 @@
#![cfg(feature = "rich_presence")]
mod set_activity;
pub use self::set_activity::*;