diff --git a/examples/presence_state.rs b/examples/presence_state.rs index 63ccf3d8..4bd2826f 100644 --- a/examples/presence_state.rs +++ b/examples/presence_state.rs @@ -1,12 +1,18 @@ use pubnub::{Keyset, PubNubClientBuilder}; +use serde::Serialize; use std::env; +#[derive(Debug, Serialize)] +struct State { + is_doing: String, +} + #[tokio::main] async fn main() -> Result<(), Box> { let publish_key = env::var("SDK_PUB_KEY")?; let subscribe_key = env::var("SDK_SUB_KEY")?; - let _client = PubNubClientBuilder::with_reqwest_transport() + let client = PubNubClientBuilder::with_reqwest_transport() .with_keyset(Keyset { subscribe_key, publish_key: Some(publish_key), @@ -17,27 +23,31 @@ async fn main() -> Result<(), Box> { println!("running!"); - // client - // .set_presence_state() - // .channels(["my_channel".into(), "other_channel".into()].to_vec()) - // .state("{\"What you're doing\": \"Me? Nothing... Just hanging around\"}") - // .user_id("user_id") - // .execute() - // .await?; - // - // let states = client - // .get_presence_state() - // .channels(["my_channel".into(), "other_channel".into()].to_vec()) - // .user_id("user_id") - // .execute() - // .await?; - // - // println!("All channels state: {:?}", states); - // - // states.iter().for_each(|channel| { - // println!("Channel: {}", channel.channel); - // println!("State: {:?}", channel.state); - // }); - // + client + .set_presence_state(State { + is_doing: "Nothing... Just hanging around...".into(), + }) + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .user_id("user_id") + .execute() + .await?; + + println!("State set!"); + println!(); + + let states = client + .get_presence_state() + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .user_id("user_id") + .execute() + .await?; + + println!("All channels state: {:?}", states); + + states.iter().for_each(|channel| { + println!("Channel: {}", channel.channel); + println!("State: {:?}", channel.state); + }); + Ok(()) } diff --git a/examples/presence_state_blocking.rs b/examples/presence_state_blocking.rs index 15010dc5..fac51485 100644 --- a/examples/presence_state_blocking.rs +++ b/examples/presence_state_blocking.rs @@ -1,11 +1,17 @@ use pubnub::{Keyset, PubNubClientBuilder}; +use serde::Serialize; use std::env; +#[derive(Debug, Serialize)] +struct State { + is_doing: String, +} + fn main() -> Result<(), Box> { let publish_key = env::var("SDK_PUB_KEY")?; let subscribe_key = env::var("SDK_SUB_KEY")?; - let _client = PubNubClientBuilder::with_reqwest_blocking_transport() + let client = PubNubClientBuilder::with_reqwest_blocking_transport() .with_keyset(Keyset { subscribe_key, publish_key: Some(publish_key), @@ -16,25 +22,29 @@ fn main() -> Result<(), Box> { println!("running!"); - // client - // .set_presence_state() - // .channels(["my_channel".into(), "other_channel".into()].to_vec()) - // .state("{\"What you're doing\": \"Me? Nothing... Just hanging around\"}") - // .user_id("user_id") - // .execute_blocking()?; - // - // let states = client - // .get_presence_state() - // .channels(["my_channel".into(), "other_channel".into()].to_vec()) - // .user_id("user_id") - // .execute_blocking()?; - // - // println!("All channels state: {:?}", states); - // - // states.iter().for_each(|channel| { - // println!("Channel: {}", channel.channel); - // println!("State: {:?}", channel.state); - // }); - // + client + .set_presence_state(State { + is_doing: "Nothing... Just hanging around...".into(), + }) + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .user_id("user_id") + .execute_blocking()?; + + println!("State set!"); + println!(); + + let states = client + .get_presence_state() + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .user_id("user_id") + .execute_blocking()?; + + println!("All channels state: {:?}", states); + + states.iter().for_each(|channel| { + println!("Channel: {}", channel.channel); + println!("State: {:?}", channel.state); + }); + Ok(()) } diff --git a/src/dx/presence/builders/get_presence_state.rs b/src/dx/presence/builders/get_presence_state.rs new file mode 100644 index 00000000..e509e13c --- /dev/null +++ b/src/dx/presence/builders/get_presence_state.rs @@ -0,0 +1,168 @@ +//! # PubNub set state module. +//! +//! The [`GetStateRequestBuilder`] lets you make and execute requests that will +//! associate the provided `state` with `user_id` on the provided list of +//! channels and channels in channel groups. + +use derive_builder::Builder; + +use crate::{ + core::{ + utils::{ + encoding::{ + url_encode_extended, url_encoded_channel_groups, url_encoded_channels, + UrlEncodeExtension, + }, + headers::{APPLICATION_JSON, CONTENT_TYPE}, + }, + Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, + }, + dx::{ + presence::{ + builders, + result::{GetStateResponseBody, GetStateResult}, + }, + pubnub_client::PubNubClientInstance, + }, + lib::{ + alloc::{ + string::{String, ToString}, + vec, + }, + collections::HashMap, + }, +}; + +/// The [`GetStateRequestBuilder`] is used to build `user_id` associated state +/// update request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`set_state`] method of the [`PubNubClient`]. +/// The [`set_state`] method is used to update state associated with `user_id` +/// on the provided channels and groups. +/// +/// [`PubNub`]:https://www.pubnub.com/ +#[derive(Builder)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct GetStateRequest { + /// Current client which can provide transportation to perform the request. + /// + /// This field is used to get [`Transport`] to perform the request. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(custom))] + pub(in crate::dx::presence) pubnub_client: PubNubClientInstance, + + /// Channels with which state will be associated. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option, into), + default = "vec![]" + )] + pub(in crate::dx::presence) channels: Vec, + + /// Channel groups with which state will be associated. + /// + /// The specified state will be associated with channels that have been + /// included in the specified target channel groups. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(into, strip_option), + default = "vec![]" + )] + pub(in crate::dx::presence) channel_groups: Vec, + + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] + /// Identifier for which `state` should be associated for provided list of + /// channels and groups. + pub(in crate::dx::presence) user_id: String, +} + +impl GetStateRequestBuilder { + /// Validate user-provided data for request builder. + /// + /// Validator ensure that list of provided data is enough to build valid + /// set state request instance. + fn validate(&self) -> Result<(), String> { + let groups_len = self.channel_groups.as_ref().map_or_else(|| 0, |v| v.len()); + let channels_len = self.channels.as_ref().map_or_else(|| 0, |v| v.len()); + + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + if channels_len == groups_len && channels_len == 0 { + Err("Either channels or channel groups should be provided".into()) + } else if self.user_id.is_none() { + Err("User id is missing".into()) + } else { + Ok(()) + } + }) + } + + /// Build [`GetStateRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl GetStateRequest { + /// Create transport request from the request builder. + pub(in crate::dx::presence) fn transport_request( + &self, + ) -> Result { + let sub_key = &self.pubnub_client.config.subscribe_key; + let mut query: HashMap = HashMap::new(); + + // Serialize list of channel groups and add into query parameters list. + url_encoded_channel_groups(&self.channel_groups) + .and_then(|channel_groups| query.insert("channel-group".into(), channel_groups)); + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}", + url_encoded_channels(&self.channels), + url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) + ), + query_parameters: query, + method: TransportMethod::Get, + headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + body: None, + }) + } +} + +impl GetStateRequestBuilder +where + T: Transport, + D: Deserializer + 'static, +{ + /// Build and call asynchronous request. + pub async fn execute(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send::(&client.transport, deserializer) + .await + } +} + +#[allow(dead_code)] +#[cfg(feature = "blocking")] +impl GetStateRequestBuilder +where + T: crate::core::blocking::Transport, + D: Deserializer + 'static, +{ + /// Build and call synchronous request. + pub fn execute_blocking(self) -> Result { + let request = self.request()?; + let transport_request = request.transport_request()?; + let client = request.pubnub_client.clone(); + let deserializer = client.deserializer.clone(); + transport_request + .send_blocking::(&client.transport, deserializer) + } +} diff --git a/src/dx/presence/builders/mod.rs b/src/dx/presence/builders/mod.rs index 0cfb4bd7..5e1636b0 100644 --- a/src/dx/presence/builders/mod.rs +++ b/src/dx/presence/builders/mod.rs @@ -25,6 +25,10 @@ pub(crate) mod here_now; pub(crate) use where_now::WhereNowRequestBuilder; pub(crate) mod where_now; +#[doc(inline)] +pub(crate) use get_presence_state::GetStateRequestBuilder; +pub(crate) mod get_presence_state; + use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; /// Validate [`PubNubClient`] configuration. diff --git a/src/dx/presence/mod.rs b/src/dx/presence/mod.rs index 7916194b..4dc05afa 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -170,6 +170,48 @@ impl PubNubClientInstance { self.heartbeat().state(state) } + /// Create a get state request builder. + /// + /// This method is used to get state associated with `user_id` on + /// channels and and channels registered with channel groups. + /// + /// Instance of [`SetStateRequestBuilder`] returned. + /// + /// # Example + /// ```rust + /// use pubnub::presence::*; + /// # use pubnub::{Keyset, PubNubClientBuilder}; + /// # use std::collections::HashMap; + /// + /// #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # use std::sync::Arc; + /// let mut pubnub = // PubNubClient + /// # PubNubClientBuilder::with_reqwest_transport() + /// # .with_keyset(Keyset { + /// # subscribe_key: "demo", + /// # publish_key: None, + /// # secret_key: None + /// # }) + /// # .with_user_id("uuid") + /// # .build()?; + /// pubnub + /// .get_presence_state() + /// .channels(["lobby".into(), "announce".into()]) + /// .channel_groups(["area-51".into()]) + /// .execute() + /// .await?; + /// # Ok(()) + /// # } + /// ``` + pub fn get_presence_state(&self) -> GetStateRequestBuilder { + GetStateRequestBuilder { + pubnub_client: Some(self.clone()), + user_id: Some(self.config.user_id.clone().to_string()), + ..Default::default() + } + } + /// Create a here now request builder. /// /// This method is used to get information about current occupancy of diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index e107719f..3b8f4444 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -239,6 +239,143 @@ impl TryFrom for SetStateResult { } } +/// The result of a get state operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetStateResult { + /// State which has been associated for `user_id` with channel(s) or channel + /// group(s). + pub states: Vec, +} + +/// Get state info for a user. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetStateInfo { + /// Channel or channel group name. + pub channel: String, + + /// State defined for the user + #[cfg(feature = "serde")] + pub state: serde_json::Value, + + /// State defined for the user + #[cfg(not(feature = "serde"))] + pub state: Vec, +} + +/// Get state service response body. +/// +/// This is a response body for a get state operation in the Presence +/// service. +/// +/// It contains information about the service that have the response, +/// operation result message and state which has been associated for +/// `user_id` with channel(s) or channel group(s). +/// +/// Also it contains information about the service that provided the response +/// and details of what exactly was wrong. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GetStateResponseBody { + /// This is a success response body for a get state operation in the + /// Presence service. + /// + /// It contains information about the service that have the response, + /// operation result message and state which has been associated for + /// `user_id` with channel(s) or channel group(s). + /// + /// # Example + /// ```json + /// { + /// "status": 200, + /// "message": "OK", + /// "payload": { + /// "channels": { + /// "channel-1": { + /// "key-1": "value-1", + /// "key-2": "value-2" + /// }, + /// "channel-2": { + /// "key-1": "value-1", + /// "key-2": "value-2" + /// } + /// }, + /// } + /// "service": "Presence" + /// } + /// ``` + SuccessResponse(APISuccessBodyWithPayload), + + /// This is an error response body for a set state operation in the Presence + /// service. + /// + /// It contains information about the service that provided the response and + /// details of what exactly was wrong. + /// + /// # Example + /// ```json + /// { + /// "error": { + /// "message": "Invalid signature", + /// "source": "grant", + /// "details": [ + /// { + /// "message": "Client and server produced different signatures for the same inputs.", + /// "location": "signature", + /// "locationType": "query" + /// } + /// ] + /// }, + /// "service": "Access Manager", + /// "status": 403 + /// } + /// ``` + ErrorResponse(APIErrorBody), +} + +/// The success result of a get state operation. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +pub struct GetStateSuccessBody { + /// State which has been associated for `user_id` with channel(s) or channel + /// group(s). + #[cfg(feature = "serde")] + pub channels: HashMap, + + /// State which has been associated for `user_id` with channel(s) or channel + /// group(s). + #[cfg(not(feature = "serde"))] + pub channels: HashMap>, +} + +impl TryFrom for GetStateResult { + type Error = PubNubError; + + fn try_from(value: GetStateResponseBody) -> Result { + match value { + GetStateResponseBody::SuccessResponse(response) => Ok(GetStateResult { + states: response + .payload + .channels + .into_iter() + .map(|(k, v)| GetStateInfo { + channel: k, + state: v, + }) + .collect(), + }), + GetStateResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +impl Deref for GetStateResult { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.states + } +} + /// The result of a here now operation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct HereNowResult { @@ -1146,4 +1283,54 @@ mod it_should { assert!(result.is_err()); } + + #[test] + fn parse_get_state_response() { + use serde_json::json; + + let input = json!({ + "status": 200, + "message": "OK", + "payload": { + "channels": { + "channel-1": { + "key-1": "value-1", + "key-2": "value-2" + }, + "channel-2": { + "key-1": "value-1", + "key-2": "value-2" + } + }, + }, + "service": "Presence" + }); + + let result: GetStateResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + result.iter().any(|channel| { + channel.channel == "channel-1" + && channel.state + == json!({ + "key-1": "value-1", + "key-2": "value-2" + }) + }); + } + + #[test] + fn parse_get_state_error_response() { + let body = GetStateResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } }