From f4f0a536762d8d2c41c91a1be05bc6a8b6cce9c0 Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 22:47:20 +0200 Subject: [PATCH 01/14] here now compiles --- examples/presence_state.rs | 43 ++ examples/where_now.rs | 29 ++ src/core/service_response.rs | 20 + src/dx/presence/builders/here_now.rs | 177 ++++++++ src/dx/presence/builders/mod.rs | 4 + src/dx/presence/mod.rs | 44 ++ src/dx/presence/result.rs | 581 ++++++++++++++++++++++++++- 7 files changed, 895 insertions(+), 3 deletions(-) create mode 100644 examples/presence_state.rs create mode 100644 examples/where_now.rs create mode 100644 src/dx/presence/builders/here_now.rs diff --git a/examples/presence_state.rs b/examples/presence_state.rs new file mode 100644 index 00000000..aceca33a --- /dev/null +++ b/examples/presence_state.rs @@ -0,0 +1,43 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +#[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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + 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); + // }); + // + Ok(()) +} diff --git a/examples/where_now.rs b/examples/where_now.rs new file mode 100644 index 00000000..7faa41d9 --- /dev/null +++ b/examples/where_now.rs @@ -0,0 +1,29 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +#[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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + println!("running!"); + + // let where_user = client + // .where_now() + // .user_id("user_id") + // .execute() + // .await?; + // + // println!("All channels data: {:?}", where_user); + + Ok(()) +} diff --git a/src/core/service_response.rs b/src/core/service_response.rs index 70c389d6..47c768da 100644 --- a/src/core/service_response.rs +++ b/src/core/service_response.rs @@ -56,6 +56,26 @@ pub struct APISuccessBodyWithMessage { pub service: String, } +/// Result of successful REST API endpoint call. +/// +/// Body contains status code, `message` and `service` response specific to used +/// endpoint. Additionaly it contains flatten `payload` which can be any generic +/// type. +/// +/// To implement deserialization for this struct - check the [`Api docs`] or docs +/// included to specific result. +/// +/// [`Api docs`]: https://www.pubnub.com/docs/sdks/rest-api +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct APISuccessBodyWithFlattenedPayload { + pub status: i32, + pub message: String, + #[cfg_attr(feature = "serde", serde(flatten))] + pub payload: D, + pub service: String, +} + /// PubNub service error response. /// /// `ErrorResponse` enum variants covers all possible [`PubNub API`] error diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs new file mode 100644 index 00000000..b5c33e65 --- /dev/null +++ b/src/dx/presence/builders/here_now.rs @@ -0,0 +1,177 @@ +//! PubNub Here Now module. +//! +//! The [`HereNowRequestBuilder`] lets you make and execute a Here Now request +//! that will associate a user with a channel. + +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, pubnub_client::PubNubClientInstance}, + lib::collections::HashMap, + presence::result::{HereNowResponseBody, HereNowResult}, +}; + +/// The Here Now request builder. +/// +/// Allows you to build a Here Now request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`here_now`] method of the [`PubNubClient`]. +/// The [`here_now`] method is used to acquire information about the current +/// state of a channel. +/// +/// [`PubNub`]: https://www.pubnub.com/ +#[derive(Builder, Debug)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct HereNowRequest { + /// 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 for which to retrieve occupancy information. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option, into), + default = "vec![]" + )] + pub(in crate::dx::presence) channels: Vec, + + /// Channel groups for which to retrieve occupancy information. + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(into, strip_option), + default = "vec![]" + )] + pub(in crate::dx::presence) channel_groups: Vec, + + /// Identifier of the user for which to retrieve occupancy information. + #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] + pub(in crate::dx::presence) user_id: String, + + /// Whether to include UUIDs of users subscribed to the channel(s). + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option), + default = "true" + )] + pub(in crate::dx::presence) include_user_id: bool, + + /// Whether to include state information of users subscribed to the channel(s). + #[builder( + field(vis = "pub(in crate::dx::presence)"), + setter(strip_option), + default = "false" + )] + pub(in crate::dx::presence) include_state: bool, +} + +impl HereNowRequestBuilder { + /// 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 [`SetStateRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl HereNowRequest { + /// 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)); + + self.include_state + .then(|| query.insert("state".into(), "1".into())); + + (!self.include_user_id).then(|| { + query.insert("disable_uuids".into(), "1".into()); + }); + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}/data", + 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 HereNowRequestBuilder +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 + } +} + +#[cfg(feature = "blocking")] +impl HereNowRequestBuilder +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) + } +} + +// TODO: unit tests for all presence requests. diff --git a/src/dx/presence/builders/mod.rs b/src/dx/presence/builders/mod.rs index 4c3a3e23..ed6986b4 100644 --- a/src/dx/presence/builders/mod.rs +++ b/src/dx/presence/builders/mod.rs @@ -17,6 +17,10 @@ pub mod set_presence_state; pub(crate) use leave::LeaveRequestBuilder; pub(crate) mod leave; +#[doc(inline)] +pub(crate) use here_now::HereNowRequestBuilder; +pub(crate) mod here_now; + 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 794b22af..cf3013de 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -169,6 +169,50 @@ impl PubNubClientInstance { { self.heartbeat().state(state) } + + /// Create a here now request builder. + /// + /// This method is used to get information about current occupancy of + /// channels and channel groups. + /// + /// Instance of [`HereNowRequestBuilder`] 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()?; + /// let response = pubnub.here_now() + /// .channels(["lobby".into()]) + /// .include_state(true) + /// .include_user_id(true) + /// .execute() + /// .await?; + /// + /// println!("All channels data: {:?}", response); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn here_now(&self) -> HereNowRequestBuilder { + HereNowRequestBuilder { + pubnub_client: Some(self.clone()), + ..Default::default() + } + } } impl PubNubClientInstance diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index a9621b51..14dc62bb 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -2,9 +2,15 @@ //! //! This module contains the [`HeartbeatResult`] type. -use crate::core::{ - service_response::{APIErrorBody, APISuccessBodyWithMessage, APISuccessBodyWithPayload}, - PubNubError, +use crate::{ + core::{ + service_response::{ + APIErrorBody, APISuccessBodyWithFlattenedPayload, APISuccessBodyWithMessage, + APISuccessBodyWithPayload, + }, + PubNubError, + }, + lib::{collections::HashMap, core::ops::Deref}, }; /// The result of a heartbeat announcement operation. @@ -233,6 +239,369 @@ impl TryFrom for SetStateResult { } } +/// The result of a here now operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HereNowResult { + /// Here now channels. + pub channels: Vec, + /// Total channels in the result. + pub total_channels: u32, + /// Amount of all users in all provided channels. + pub total_occupancy: u32, +} + +/// The here now channel data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HereNowChannel { + /// Name of the channel + pub name: String, + + /// Amount of users in the channel + pub occupancy: u32, + + /// Users data + pub occupants: Vec, +} + +/// The here now user data. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HereNowUser { + /// User's id + pub user_id: String, + + /// State defined for the user + #[cfg(feature = "serde")] + pub state: Option, + + /// State defined for the user + #[cfg(not(feature = "serde"))] + pub state: Option>, +} + +/// Here now service response body for here now. +/// This is a success response body for a here now operation in The +/// Presence service. +/// +/// It contains information about the success of the operation, the service that +/// provided the response, and the result of the operation. +/// +/// Also it can contain information about the occupancy of the channel(s) Or +/// channel group(s) and the list of user id(s) subscribed. +/// +/// Additionally, it can provide error information if the operation failed. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HereNowResponseBody { + /// This is a success response body for a here now operation in the + /// Presence service. + /// + /// It contains information about the success of the operation, the service + /// that provided the response, and the result of the operation. + SuccessResponse(HereNowResponseSuccessBody), + + /// This is an error response body for a here now 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), +} + +/// Possible variants of HereNow service success response body. +/// +/// Check [HereNowResponseBody] for more details. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HereNowResponseSuccessBody { + /// Single channel + /// + /// # Example + /// + /// without state: + /// + /// ```json + /// { + /// "status":200, + /// "message":"OK", + /// "occupancy":1, + /// "uuids":[ + /// "just_me" + /// ], + /// "service":"Presence" + /// } + /// ``` + /// + /// with state: + /// + /// ```json + /// { + /// "message": "OK", + /// "occupancy": 3, + /// "service": "Presence", + /// "status": 200, + /// "uuids": [ + /// { + /// "state": { + /// "channel1-state": [ + /// "channel-1-random-value" + /// ] + /// }, + /// "uuid": "Earline" + /// }, + /// { + /// "state": { + /// "channel1-state": [ + /// "channel-1-random-value" + /// ] + /// }, + /// "uuid": "Glen" + /// } + /// ] + /// } + /// ``` + SingleChannel(APISuccessBodyWithFlattenedPayload), + + /// Multiple channels + /// + /// # Example + /// + /// without state: + /// ```json + /// { + /// "status":200, + /// "message":"OK", + /// "payload":{ + /// "channels":{ + /// "my_channel":{ + /// "occupancy":1, + /// "uuids":[ + /// "pn-200543f2-b394-4909-9e7b-987848e44729" + /// ] + /// }, + /// "kekw":{ + /// "occupancy":1, + /// "uuids":[ + /// "just_me" + /// ] + /// } + /// }, + /// "total_channels":2, + /// "total_occupancy":2 + /// }, + /// "service":"Presence" + /// } + /// ``` + /// + /// with state: + /// ```json + /// { + /// "message": "OK", + /// "payload": { + /// "channels": { + /// "test-channel1": { + /// "occupancy": 1, + /// "uuids": [ + /// { + /// "state": { + /// "channel1-state": [ + /// "channel-1-random-value" + /// ] + /// }, + /// "uuid": "Kim" + /// } + /// ] + /// }, + /// "test-channel2": { + /// "occupancy": 1, + /// "uuids": [ + /// { + /// "state": { + /// "channel2-state": [ + /// "channel-2-random-value" + /// ] + /// }, + /// "uuid": "Earline" + /// } + /// ] + /// } + /// }, + /// "total_channels": 3, + /// "total_occupancy": 3 + /// }, + /// "service": "Presence", + /// "status": 200 + /// } + /// ``` + MultipleChannels(APISuccessBodyWithPayload), +} + +/// Channels in here now response. +/// +/// Check [`HereNowResponseSuccessBody`] for more details. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HereNowResponseChannels { + /// Amount of channels in response. + pub total_channels: u32, + + /// Amount of users in response. + pub total_occupancy: u32, + + /// List of channels in response. + pub channels: HashMap, +} + +/// Channel description in here now response. +/// +/// Check [`HereNowResponseSuccessBody`] for more details. +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HereNowResponseChannelIdentifier { + /// Amount of users in channel. + pub occupancy: u32, + + /// List of users in channel. + pub uuids: Vec, +} + +/// Possible variants of user identifier in here now response. +/// +/// Check [`HereNowResponseSuccessBody`] for more details. +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HereNowResponseUserIdentifier { + /// User identifier is a string. + String(String), + + /// User identifier is a map of channel names to their states. + WithState { + /// User identifier. + uuid: String, + + /// User state. + #[cfg(feature = "serde")] + state: serde_json::Value, + + /// User state. + #[cfg(not(feature = "serde"))] + state: Vec, + }, +} + +impl TryFrom for HereNowResult { + type Error = PubNubError; + + fn try_from(value: HereNowResponseBody) -> Result { + match value { + HereNowResponseBody::SuccessResponse(resp) => Ok(match resp { + HereNowResponseSuccessBody::SingleChannel(single) => { + let occupancy = single.payload.occupancy; + + let occupants = single + .payload + .uuids + .iter() + .map(|uuid| match uuid { + HereNowResponseUserIdentifier::String(uuid) => HereNowUser { + user_id: uuid.clone(), + state: None, + }, + HereNowResponseUserIdentifier::WithState { uuid, state } => { + HereNowUser { + user_id: uuid.clone(), + state: Some(state.clone()), + } + } + }) + .collect(); + + let channels = vec![HereNowChannel { + name: "".into(), + occupancy, + occupants, + }]; + + Self { + channels, + total_channels: 1, + total_occupancy: occupancy, + } + } + HereNowResponseSuccessBody::MultipleChannels(multiple) => { + let total_channels = multiple.payload.total_channels; + let total_occupancy = multiple.payload.total_occupancy; + + let channels = multiple + .payload + .channels + .into_iter() + .map(|(name, channel)| { + let occupancy = channel.occupancy; + + let occupants = channel + .uuids + .into_iter() + .map(|uuid| match uuid { + HereNowResponseUserIdentifier::String(uuid) => HereNowUser { + user_id: uuid.clone(), + state: None, + }, + HereNowResponseUserIdentifier::WithState { uuid, state } => { + HereNowUser { + user_id: uuid.clone(), + state: Some(state.clone()), + } + } + }) + .collect(); + + HereNowChannel { + name, + occupancy, + occupants, + } + }) + .collect(); + + Self { + channels, + total_channels, + total_occupancy, + } + } + }), + HereNowResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +impl Deref for HereNowResult { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.channels + } +} + #[cfg(test)] mod it_should { use std::collections::HashMap; @@ -326,4 +695,210 @@ mod it_should { assert!(result.is_err()); } + + #[test] + fn parse_here_now_response_single_channel() { + use serde_json::json; + + let input = json!({ + "status":200, + "message":"OK", + "occupancy":1, + "uuids":[ + "just_me" + ], + "service":"Presence" + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 1); + assert_eq!(result.total_occupancy, 1); + assert_eq!(result.len(), 1); + assert_eq!(result.first().unwrap().name, ""); + assert_eq!(result.first().unwrap().occupancy, 1); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().user_id, + "just_me" + ); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().state, + None + ); + } + + #[test] + fn parse_here_now_response_single_channel_with_state() { + use serde_json::json; + + let input = json!({ + "message": "OK", + "occupancy": 3, + "service": "Presence", + "status": 200, + "uuids": [ + { + "state": { + "channel1-state": [ + "channel-1-random-value" + ] + }, + "uuid": "Earline" + }, + { + "state": { + "channel1-state": [ + "channel-1-random-value" + ] + }, + "uuid": "Glen" + } + ] + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 1); + assert_eq!(result.total_occupancy, 2); + assert_eq!(result.len(), 1); + assert_eq!(result.first().unwrap().name, ""); + assert_eq!(result.first().unwrap().occupancy, 2); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().user_id, + "just_me" + ); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().state, + Some(json!({"channel1-state": ["channel-1-random-value"]})) + ); + } + + #[test] + fn parse_here_now_response_multiple_channels() { + use serde_json::json; + + let input = json!({ + "status":200, + "message":"OK", + "payload":{ + "channels":{ + "my_channel":{ + "occupancy":1, + "uuids":[ + "pn-200543f2-b394-4909-9e7b-987848e44729" + ] + }, + "kekw":{ + "occupancy":1, + "uuids":[ + "just_me" + ] + } + }, + "total_channels":2, + "total_occupancy":2 + }, + "service":"Presence" + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 2); + assert_eq!(result.total_occupancy, 2); + assert_eq!(result.len(), 2); + assert_eq!(result.first().unwrap().name, "my_channel"); + assert_eq!(result.first().unwrap().occupancy, 1); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().user_id, + "pn-200543f2-b394-4909-9e7b-987848e44729" + ); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().state, + None + ); + } + + #[test] + fn parse_here_now_response_multiple_channels_with_state() { + use serde_json::json; + + let input = json!({ + "message": "OK", + "payload": { + "channels": { + "test-channel1": { + "occupancy": 1, + "uuids": [ + { + "state": { + "channel1-state": [ + "channel-1-random-value" + ] + }, + "uuid": "Kim" + } + ] + }, + "test-channel2": { + "occupancy": 1, + "uuids": [ + { + "state": { + "channel2-state": [ + "channel-2-random-value" + ] + }, + "uuid": "Earline" + } + ] + } + }, + "total_channels": 3, + "total_occupancy": 3 + }, + "service": "Presence", + "status": 200 + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 2); + assert_eq!(result.total_occupancy, 2); + assert_eq!(result.len(), 2); + assert_eq!(result.first().unwrap().name, "test-channel1"); + assert_eq!(result.first().unwrap().occupancy, 1); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().user_id, + "Kim" + ); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().state, + Some(json!({"channel1-state": ["channel-1-random-value"]})) + ); + } + + #[test] + fn parse_here_now_error_response() { + let body = HereNowResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } } From 06aa114f99a009126bba134238f7dc68a7615811 Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 21:39:17 +0200 Subject: [PATCH 02/14] some tests passing --- src/dx/presence/result.rs | 86 ++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 14dc62bb..86e12a2a 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -736,7 +736,7 @@ mod it_should { let input = json!({ "message": "OK", - "occupancy": 3, + "occupancy": 2, "service": "Presence", "status": 200, "uuids": [ @@ -767,16 +767,20 @@ mod it_should { assert_eq!(result.total_channels, 1); assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 1); - assert_eq!(result.first().unwrap().name, ""); - assert_eq!(result.first().unwrap().occupancy, 2); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().user_id, - "just_me" - ); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().state, - Some(json!({"channel1-state": ["channel-1-random-value"]})) - ); + assert!(result.iter().find(|channel| channel.name == "").is_some()); + assert!(result + .iter() + .find(|channel| channel.occupancy == 2) + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().user_id == "Earline") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().state + == Some(json!({"channel1-state": ["channel-1-random-value"]}))) + .is_some()); } #[test] @@ -815,16 +819,24 @@ mod it_should { assert_eq!(result.total_channels, 2); assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 2); - assert_eq!(result.first().unwrap().name, "my_channel"); - assert_eq!(result.first().unwrap().occupancy, 1); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().user_id, - "pn-200543f2-b394-4909-9e7b-987848e44729" - ); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().state, - None - ); + + assert!(result + .iter() + .find(|channel| channel.name == "my_channel") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupancy == 1) + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().user_id + == "pn-200543f2-b394-4909-9e7b-987848e44729") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().state.is_none()) + .is_some()); } #[test] @@ -862,8 +874,8 @@ mod it_should { ] } }, - "total_channels": 3, - "total_occupancy": 3 + "total_channels": 2, + "total_occupancy": 2 }, "service": "Presence", "status": 200 @@ -877,16 +889,24 @@ mod it_should { assert_eq!(result.total_channels, 2); assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 2); - assert_eq!(result.first().unwrap().name, "test-channel1"); - assert_eq!(result.first().unwrap().occupancy, 1); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().user_id, - "Kim" - ); - assert_eq!( - result.first().unwrap().occupants.first().unwrap().state, - Some(json!({"channel1-state": ["channel-1-random-value"]})) - ); + + assert!(result + .iter() + .find(|channel| channel.name == "test-channel1") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupancy == 1) + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().user_id == "Kim") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().state + == Some(json!({"channel1-state": ["channel-1-random-value"]}))) + .is_some()); } #[test] From a28e2e8691043cca3264a09607111bf5344eecac Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 22:12:34 +0200 Subject: [PATCH 03/14] working here now --- src/core/transport_request.rs | 1 + src/dx/presence/builders/here_now.rs | 30 ++++++++--- src/dx/presence/result.rs | 81 ++++++++++++++++++---------- 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/src/core/transport_request.rs b/src/core/transport_request.rs index 77f6cc0a..7fc4d47a 100644 --- a/src/core/transport_request.rs +++ b/src/core/transport_request.rs @@ -123,6 +123,7 @@ impl TransportRequest { { // Request configured endpoint. let response = transport.send(self.clone()).await?; + Self::deserialize( response.clone(), Box::new(move |bytes| deserializer.deserialize(bytes)), diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index b5c33e65..edba0152 100644 --- a/src/dx/presence/builders/here_now.rs +++ b/src/dx/presence/builders/here_now.rs @@ -3,6 +3,8 @@ //! The [`HereNowRequestBuilder`] lets you make and execute a Here Now request //! that will associate a user with a channel. +use core::ops::Deref; + use derive_builder::Builder; use crate::{ @@ -59,10 +61,6 @@ pub struct HereNowRequest { )] pub(in crate::dx::presence) channel_groups: Vec, - /// Identifier of the user for which to retrieve occupancy information. - #[builder(field(vis = "pub(in crate::dx::presence)"), setter(strip_option, into))] - pub(in crate::dx::presence) user_id: String, - /// Whether to include UUIDs of users subscribed to the channel(s). #[builder( field(vis = "pub(in crate::dx::presence)"), @@ -92,8 +90,6 @@ impl HereNowRequestBuilder { 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(()) } @@ -126,11 +122,15 @@ impl HereNowRequest { query.insert("disable_uuids".into(), "1".into()); }); + query.insert( + "uuid".into(), + self.pubnub_client.config.user_id.deref().clone(), + ); + Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}/data", + "/v2/presence/sub-key/{sub_key}/channel/{}", url_encoded_channels(&self.channels), - url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) ), query_parameters: query, method: TransportMethod::Get, @@ -147,13 +147,27 @@ where { /// Build and call asynchronous request. pub async fn execute(self) -> Result { + let name_replacement = self + .channels + .as_ref() + .map(|channels| (channels.len() == 1).then(|| channels[0].clone())) + .flatten(); + 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 + .map(|mut result: HereNowResult| { + name_replacement.is_some().then(|| { + result.channels[0].name = name_replacement.expect("Cannot be None"); + }); + + result + }) } } diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 86e12a2a..6d2ff9a3 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -480,7 +480,7 @@ pub struct HereNowResponseChannelIdentifier { pub occupancy: u32, /// List of users in channel. - pub uuids: Vec, + pub uuids: Option>, } /// Possible variants of user identifier in here now response. @@ -492,6 +492,12 @@ pub enum HereNowResponseUserIdentifier { /// User identifier is a string. String(String), + /// User identifier is a map of uuids + Map { + /// User identifier. + uuid: String, + }, + /// User identifier is a map of channel names to their states. WithState { /// User identifier. @@ -519,20 +525,28 @@ impl TryFrom for HereNowResult { let occupants = single .payload .uuids - .iter() - .map(|uuid| match uuid { - HereNowResponseUserIdentifier::String(uuid) => HereNowUser { - user_id: uuid.clone(), - state: None, - }, - HereNowResponseUserIdentifier::WithState { uuid, state } => { - HereNowUser { - user_id: uuid.clone(), - state: Some(state.clone()), - } - } + .map(|maybe_uuids| { + maybe_uuids + .iter() + .map(|uuid| match uuid { + HereNowResponseUserIdentifier::String(uuid) => HereNowUser { + user_id: uuid.clone(), + state: None, + }, + HereNowResponseUserIdentifier::Map { uuid } => HereNowUser { + user_id: uuid.clone(), + state: None, + }, + HereNowResponseUserIdentifier::WithState { uuid, state } => { + HereNowUser { + user_id: uuid.clone(), + state: Some(state.clone()), + } + } + }) + .collect() }) - .collect(); + .unwrap_or_default(); let channels = vec![HereNowChannel { name: "".into(), @@ -559,20 +573,33 @@ impl TryFrom for HereNowResult { let occupants = channel .uuids - .into_iter() - .map(|uuid| match uuid { - HereNowResponseUserIdentifier::String(uuid) => HereNowUser { - user_id: uuid.clone(), - state: None, - }, - HereNowResponseUserIdentifier::WithState { uuid, state } => { - HereNowUser { - user_id: uuid.clone(), - state: Some(state.clone()), - } - } + .map(|maybe_uuids| { + maybe_uuids + .into_iter() + .map(|uuid| match uuid { + HereNowResponseUserIdentifier::String(uuid) => { + HereNowUser { + user_id: uuid.clone(), + state: None, + } + } + HereNowResponseUserIdentifier::Map { uuid } => { + HereNowUser { + user_id: uuid.clone(), + state: None, + } + } + HereNowResponseUserIdentifier::WithState { + uuid, + state, + } => HereNowUser { + user_id: uuid.clone(), + state: Some(state.clone()), + }, + }) + .collect() }) - .collect(); + .unwrap_or_default(); HereNowChannel { name, From f6bae630bdc9ac195c689479aae3ff97e9c815fd Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 22:30:31 +0200 Subject: [PATCH 04/14] proper order --- src/dx/presence/result.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 6d2ff9a3..9fc0643d 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -492,12 +492,6 @@ pub enum HereNowResponseUserIdentifier { /// User identifier is a string. String(String), - /// User identifier is a map of uuids - Map { - /// User identifier. - uuid: String, - }, - /// User identifier is a map of channel names to their states. WithState { /// User identifier. @@ -511,6 +505,12 @@ pub enum HereNowResponseUserIdentifier { #[cfg(not(feature = "serde"))] state: Vec, }, + + /// User identifier is a map of uuids + Map { + /// User identifier. + uuid: String, + }, } impl TryFrom for HereNowResult { From 037187c5c7bd475cc48d88d815ddbbfc05d431b4 Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 22:50:12 +0200 Subject: [PATCH 05/14] I fucked up something with rebase but seems to be working --- examples/here_now.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/here_now.rs diff --git a/examples/here_now.rs b/examples/here_now.rs new file mode 100644 index 00000000..ac9d204d --- /dev/null +++ b/examples/here_now.rs @@ -0,0 +1,42 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +#[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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + println!("running!"); + + let channels_now = client + .here_now() + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .include_state(true) + .include_user_id(true) + .execute() + .await?; + + println!("All channels data: {:?}", channels_now); + + channels_now.iter().for_each(|channel| { + println!("Channel: {}", channel.name); + println!("Occupancy: {}", channel.occupancy); + println!("Occupants: {:?}", channel.occupants); + + channel + .occupants + .iter() + .for_each(|occupant| println!("Occupant: {:?}", occupant)); + }); + + Ok(()) +} From fa10adf1da10f93af115182813adbdb085ca8d4f Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 22:57:43 +0200 Subject: [PATCH 06/14] add lacking unit tests for parsing --- src/dx/presence/result.rs | 90 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 9fc0643d..3d1d03e1 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -936,6 +936,96 @@ mod it_should { .is_some()); } + #[test] + fn parse_here_now_response_single_channel_with_map_uuid() { + use serde_json::json; + + let input = json!({ + "status":200, + "message":"OK", + "occupancy":1, + "uuids":[ + {"uuid":"just_me"} + ], + "service":"Presence" + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 1); + assert_eq!(result.total_occupancy, 1); + assert_eq!(result.len(), 1); + assert_eq!(result.first().unwrap().name, ""); + assert_eq!(result.first().unwrap().occupancy, 1); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().user_id, + "just_me" + ); + assert_eq!( + result.first().unwrap().occupants.first().unwrap().state, + None + ); + } + + #[test] + fn parse_here_now_response_multiple_channels_with_map_uuid() { + use serde_json::json; + + let input = json!({ + "status":200, + "message":"OK", + "payload":{ + "channels":{ + "my_channel":{ + "occupancy":1, + "uuids":[ + {"uuid":"pn-200543f2-b394-4909-9e7b-987848e44729"} + ] + }, + "kekw":{ + "occupancy":1, + "uuids":[ + {"uuid":"just_me"} + ] + } + }, + "total_channels":2, + "total_occupancy":2 + }, + "service":"Presence" + }); + + let result: HereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(result.total_channels, 2); + assert_eq!(result.total_occupancy, 2); + assert_eq!(result.len(), 2); + + assert!(result + .iter() + .find(|channel| channel.name == "my_channel") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupancy == 1) + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().user_id + == "pn-200543f2-b394-4909-9e7b-987848e44729") + .is_some()); + assert!(result + .iter() + .find(|channel| channel.occupants.first().unwrap().state.is_none()) + .is_some()); + } + #[test] fn parse_here_now_error_response() { let body = HereNowResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { From f16c320c575076938c02b13d165f9ab9c6fe965e Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 23:04:39 +0200 Subject: [PATCH 07/14] clippy --- examples/presence_state.rs | 2 +- examples/where_now.rs | 2 +- src/dx/presence/builders/here_now.rs | 8 +--- src/dx/presence/result.rs | 69 ++++++++-------------------- 4 files changed, 24 insertions(+), 57 deletions(-) diff --git a/examples/presence_state.rs b/examples/presence_state.rs index aceca33a..63ccf3d8 100644 --- a/examples/presence_state.rs +++ b/examples/presence_state.rs @@ -6,7 +6,7 @@ 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), diff --git a/examples/where_now.rs b/examples/where_now.rs index 7faa41d9..8b22ef90 100644 --- a/examples/where_now.rs +++ b/examples/where_now.rs @@ -6,7 +6,7 @@ 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), diff --git a/src/dx/presence/builders/here_now.rs b/src/dx/presence/builders/here_now.rs index edba0152..bc6d3b7f 100644 --- a/src/dx/presence/builders/here_now.rs +++ b/src/dx/presence/builders/here_now.rs @@ -10,10 +10,7 @@ use derive_builder::Builder; use crate::{ core::{ utils::{ - encoding::{ - url_encode_extended, url_encoded_channel_groups, url_encoded_channels, - UrlEncodeExtension, - }, + encoding::{url_encoded_channel_groups, url_encoded_channels}, headers::{APPLICATION_JSON, CONTENT_TYPE}, }, Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, @@ -150,8 +147,7 @@ where let name_replacement = self .channels .as_ref() - .map(|channels| (channels.len() == 1).then(|| channels[0].clone())) - .flatten(); + .and_then(|channels| (channels.len() == 1).then(|| channels[0].clone())); let request = self.request()?; let transport_request = request.transport_request()?; diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 3d1d03e1..7cf33517 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -794,20 +794,15 @@ mod it_should { assert_eq!(result.total_channels, 1); assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 1); - assert!(result.iter().find(|channel| channel.name == "").is_some()); + assert!(result.iter().any(|channel| channel.name.is_empty())); + assert!(result.iter().any(|channel| channel.occupancy == 2)); assert!(result .iter() - .find(|channel| channel.occupancy == 2) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().user_id == "Earline")); assert!(result .iter() - .find(|channel| channel.occupants.first().unwrap().user_id == "Earline") - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupants.first().unwrap().state - == Some(json!({"channel1-state": ["channel-1-random-value"]}))) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().state + == Some(json!({"channel1-state": ["channel-1-random-value"]})))); } #[test] @@ -847,23 +842,15 @@ mod it_should { assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 2); + assert!(result.iter().any(|channel| channel.name == "my_channel")); + assert!(result.iter().any(|channel| channel.occupancy == 1)); assert!(result .iter() - .find(|channel| channel.name == "my_channel") - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupancy == 1) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().user_id + == "pn-200543f2-b394-4909-9e7b-987848e44729")); assert!(result .iter() - .find(|channel| channel.occupants.first().unwrap().user_id - == "pn-200543f2-b394-4909-9e7b-987848e44729") - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupants.first().unwrap().state.is_none()) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().state.is_none())); } #[test] @@ -917,23 +904,15 @@ mod it_should { assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 2); + assert!(result.iter().any(|channel| channel.name == "test-channel1")); + assert!(result.iter().any(|channel| channel.occupancy == 1)); assert!(result .iter() - .find(|channel| channel.name == "test-channel1") - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupancy == 1) - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupants.first().unwrap().user_id == "Kim") - .is_some()); + .any(|channel| channel.occupants.first().unwrap().user_id == "Kim")); assert!(result .iter() - .find(|channel| channel.occupants.first().unwrap().state - == Some(json!({"channel1-state": ["channel-1-random-value"]}))) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().state + == Some(json!({"channel1-state": ["channel-1-random-value"]})))); } #[test] @@ -1007,23 +986,15 @@ mod it_should { assert_eq!(result.total_occupancy, 2); assert_eq!(result.len(), 2); + assert!(result.iter().any(|channel| channel.name == "my_channel")); + assert!(result.iter().any(|channel| channel.occupancy == 1)); assert!(result .iter() - .find(|channel| channel.name == "my_channel") - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupancy == 1) - .is_some()); - assert!(result - .iter() - .find(|channel| channel.occupants.first().unwrap().user_id - == "pn-200543f2-b394-4909-9e7b-987848e44729") - .is_some()); + .any(|channel| channel.occupants.first().unwrap().user_id + == "pn-200543f2-b394-4909-9e7b-987848e44729")); assert!(result .iter() - .find(|channel| channel.occupants.first().unwrap().state.is_none()) - .is_some()); + .any(|channel| channel.occupants.first().unwrap().state.is_none())); } #[test] From bc662ef2014bccf2914101f193089bea3b665fbf Mon Sep 17 00:00:00 2001 From: Xavrax Date: Tue, 29 Aug 2023 23:17:17 +0200 Subject: [PATCH 08/14] proper examples --- Cargo.toml | 24 +++++++++++++++++ examples/here_now.rs | 4 ++- examples/here_now_blocking.rs | 42 +++++++++++++++++++++++++++++ examples/presence_state_blocking.rs | 40 +++++++++++++++++++++++++++ examples/where_now_blocking.rs | 27 +++++++++++++++++++ 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 examples/here_now_blocking.rs create mode 100644 examples/presence_state_blocking.rs create mode 100644 examples/where_now_blocking.rs diff --git a/Cargo.toml b/Cargo.toml index 2336e90d..f04e745a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,3 +171,27 @@ required-features = ["default", "subscribe"] name = "subscribe_raw_blocking" required-features = ["default", "subscribe"] +[[example]] +name = "here_now" +required-features = ["default", "presence"] + +[[example]] +name = "here_now_blocking" +required-features = ["default", "blocking", "presence"] + +[[example]] +name = "where_now" +required-features = ["default", "presence"] + +[[example]] +name = "where_now_blocking" +required-features = ["default", "blocking", "presence"] + +[[example]] +name = "presence_state" +required-features = ["default", "presence"] + +[[example]] +name = "presence_state_blocking" +required-features = ["default", "blocking", "presence"] + diff --git a/examples/here_now.rs b/examples/here_now.rs index ac9d204d..0b0874aa 100644 --- a/examples/here_now.rs +++ b/examples/here_now.rs @@ -25,7 +25,7 @@ async fn main() -> Result<(), Box> { .execute() .await?; - println!("All channels data: {:?}", channels_now); + println!("All channels data: {:?}\n", channels_now); channels_now.iter().for_each(|channel| { println!("Channel: {}", channel.name); @@ -36,6 +36,8 @@ async fn main() -> Result<(), Box> { .occupants .iter() .for_each(|occupant| println!("Occupant: {:?}", occupant)); + + println!(); }); Ok(()) diff --git a/examples/here_now_blocking.rs b/examples/here_now_blocking.rs new file mode 100644 index 00000000..387a262c --- /dev/null +++ b/examples/here_now_blocking.rs @@ -0,0 +1,42 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + println!("running!"); + + let channels_now = client + .here_now() + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .include_state(true) + .include_user_id(true) + .execute_blocking()?; + + println!("All channels data: {:?}\n", channels_now); + + channels_now.iter().for_each(|channel| { + println!("Channel: {}", channel.name); + println!("Occupancy: {}", channel.occupancy); + println!("Occupants: {:?}", channel.occupants); + + channel + .occupants + .iter() + .for_each(|occupant| println!("Occupant: {:?}", occupant)); + + println!(); + }); + + Ok(()) +} diff --git a/examples/presence_state_blocking.rs b/examples/presence_state_blocking.rs new file mode 100644 index 00000000..15010dc5 --- /dev/null +++ b/examples/presence_state_blocking.rs @@ -0,0 +1,40 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + 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); + // }); + // + Ok(()) +} diff --git a/examples/where_now_blocking.rs b/examples/where_now_blocking.rs new file mode 100644 index 00000000..c67d92d7 --- /dev/null +++ b/examples/where_now_blocking.rs @@ -0,0 +1,27 @@ +use pubnub::{Keyset, PubNubClientBuilder}; +use std::env; + +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() + .with_keyset(Keyset { + subscribe_key, + publish_key: Some(publish_key), + secret_key: None, + }) + .with_user_id("user_id") + .build()?; + + println!("running!"); + + // let where_user = client + // .where_now() + // .user_id("user_id") + // .execute_blocking()?; + // + // println!("All channels data: {:?}", where_user); + + Ok(()) +} From 6e02842f63a26a4aa615338602bdc5c67ba53f11 Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 00:48:33 +0200 Subject: [PATCH 09/14] tests pass --- examples/where_now.rs | 12 +-- examples/where_now_blocking.rs | 11 +-- src/dx/presence/builders/mod.rs | 4 + src/dx/presence/builders/where_now.rs | 131 ++++++++++++++++++++++++ src/dx/presence/mod.rs | 40 ++++++++ src/dx/presence/result.rs | 137 ++++++++++++++++++++++++++ 6 files changed, 320 insertions(+), 15 deletions(-) create mode 100644 src/dx/presence/builders/where_now.rs diff --git a/examples/where_now.rs b/examples/where_now.rs index 8b22ef90..76eb227a 100644 --- a/examples/where_now.rs +++ b/examples/where_now.rs @@ -6,7 +6,7 @@ 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,13 +17,9 @@ async fn main() -> Result<(), Box> { println!("running!"); - // let where_user = client - // .where_now() - // .user_id("user_id") - // .execute() - // .await?; - // - // println!("All channels data: {:?}", where_user); + let where_user = client.where_now().user_id("user_id").execute().await?; + + println!("User channels: {:?}", where_user); Ok(()) } diff --git a/examples/where_now_blocking.rs b/examples/where_now_blocking.rs index c67d92d7..24764ba6 100644 --- a/examples/where_now_blocking.rs +++ b/examples/where_now_blocking.rs @@ -5,7 +5,7 @@ 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_blocking_transport() .with_keyset(Keyset { subscribe_key, publish_key: Some(publish_key), @@ -16,12 +16,9 @@ fn main() -> Result<(), Box> { println!("running!"); - // let where_user = client - // .where_now() - // .user_id("user_id") - // .execute_blocking()?; - // - // println!("All channels data: {:?}", where_user); + let where_user = client.where_now().user_id("user_id").execute_blocking()?; + + println!("User channels: {:?}", where_user); Ok(()) } diff --git a/src/dx/presence/builders/mod.rs b/src/dx/presence/builders/mod.rs index ed6986b4..0cfb4bd7 100644 --- a/src/dx/presence/builders/mod.rs +++ b/src/dx/presence/builders/mod.rs @@ -21,6 +21,10 @@ pub(crate) mod leave; pub(crate) use here_now::HereNowRequestBuilder; pub(crate) mod here_now; +#[doc(inline)] +pub(crate) use where_now::WhereNowRequestBuilder; +pub(crate) mod where_now; + use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String}; /// Validate [`PubNubClient`] configuration. diff --git a/src/dx/presence/builders/where_now.rs b/src/dx/presence/builders/where_now.rs new file mode 100644 index 00000000..e0808908 --- /dev/null +++ b/src/dx/presence/builders/where_now.rs @@ -0,0 +1,131 @@ +//! PubNub Where Now module. +//! +//! The [`WhereNowRequestBuilder`] lets you make and execute a Here Now request +//! that will associate a user with a channel. + +use core::ops::Deref; + +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, pubnub_client::PubNubClientInstance}, + lib::collections::HashMap, + presence::result::{WhereNowResponseBody, WhereNowResult}, +}; + +/// The Here Now request builder. +/// +/// Allows you to build a Here Now request that is sent to the [`PubNub`] network. +/// +/// This struct is used by the [`here_now`] method of the [`PubNubClient`]. +/// The [`here_now`] method is used to acquire information about the current +/// state of a channel. +/// +/// [`PubNub`]: https://www.pubnub.com/ +#[derive(Builder, Debug)] +#[builder( + pattern = "owned", + build_fn(vis = "pub(in crate::dx::presence)", validate = "Self::validate"), + no_std +)] +pub struct WhereNowRequest { + /// 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, + + #[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 WhereNowRequestBuilder { + /// 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> { + builders::validate_configuration(&self.pubnub_client).and_then(|_| { + self.user_id + .clone() + .is_some_and(|id| !id.is_empty()) + .then(|| ()) + .ok_or_else(|| { + "User ID is empty. It should be provided and not empty string.".to_owned() + }) + }) + } + + /// Build [`SetStateRequest`] from builder. + fn request(self) -> Result, PubNubError> { + self.build() + .map_err(|err| PubNubError::general_api_error(err.to_string(), None, None)) + } +} + +impl WhereNowRequest { + /// 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; + + Ok(TransportRequest { + path: format!( + "/v2/presence/sub-key/{sub_key}/uuid/{}", + url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) + ), + query_parameters: HashMap::new(), + method: TransportMethod::Get, + headers: [(CONTENT_TYPE.into(), APPLICATION_JSON.into())].into(), + body: None, + }) + } +} + +impl WhereNowRequestBuilder +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 + } +} + +#[cfg(feature = "blocking")] +impl WhereNowRequestBuilder +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/mod.rs b/src/dx/presence/mod.rs index cf3013de..7916194b 100644 --- a/src/dx/presence/mod.rs +++ b/src/dx/presence/mod.rs @@ -213,6 +213,46 @@ impl PubNubClientInstance { ..Default::default() } } + + /// Create a where now request builder. + /// + /// This method is used to get information about channels where `user_id` + /// is currently present. + /// + /// Instance of [`WhereNowRequestBuilder`] 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()?; + /// let response = pubnub.where_now().user_id("user_id").execute().await?; + + /// + /// println!("User channels: {:?}", response); + /// + /// # Ok(()) + /// # } + /// ``` + pub fn where_now(&self) -> WhereNowRequestBuilder { + WhereNowRequestBuilder { + pubnub_client: Some(self.clone()), + ..Default::default() + } + } } impl PubNubClientInstance diff --git a/src/dx/presence/result.rs b/src/dx/presence/result.rs index 7cf33517..e107719f 100644 --- a/src/dx/presence/result.rs +++ b/src/dx/presence/result.rs @@ -629,6 +629,104 @@ impl Deref for HereNowResult { } } +/// The result of a here now operation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WhereNowResult { + /// Here now channels. + pub channels: Vec, +} + +/// Where now service response body for where now. +/// This is a success response body for a where now operation in The +/// Presence service. +/// +/// It contains information about the success of the operation, the service that +/// provided the response, and the result of the operation. +/// +/// It also contains information about the channels that the user is currently +/// subscribed to. +/// +/// Additionally, it can provide error information if the operation failed. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize), serde(untagged))] +pub enum WhereNowResponseBody { + /// This is a success response body for a where now operation in the + /// Presence service. + /// + /// It contains information about the success of the operation, the service + /// that provided the response, and the result of the operation. + SuccessResponse(APISuccessBodyWithPayload), + + /// This is an error response body for a where now 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 result of a where now operation. +/// +/// # Example +/// ```json +/// { +/// "status":200, +/// "message":"OK", +/// "payload":{ +/// "channels":[ +/// "my_channel" +/// ] +/// }, +/// "service":"Presence" +/// } +/// ``` +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WhereNowResponseSuccessBody { + /// Channels that the user is currently subscribed to. + pub channels: Vec, +} + +impl TryFrom for WhereNowResult { + type Error = PubNubError; + + fn try_from(value: WhereNowResponseBody) -> Result { + match value { + WhereNowResponseBody::SuccessResponse(resp) => Ok(Self { + channels: resp.payload.channels, + }), + WhereNowResponseBody::ErrorResponse(resp) => Err(resp.into()), + } + } +} + +impl Deref for WhereNowResult { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.channels + } +} + #[cfg(test)] mod it_should { use std::collections::HashMap; @@ -1009,4 +1107,43 @@ mod it_should { assert!(result.is_err()); } + + #[test] + fn parse_where_now_response() { + use serde_json::json; + + let input = json!({ + "status":200, + "message":"OK", + "payload":{ + "channels":[ + "my_channel" + ] + }, + "service":"Presence" + }); + + let result: WhereNowResult = serde_json::from_value::(input) + .unwrap() + .try_into() + .unwrap(); + + result + .channels + .iter() + .any(|channel| channel == "my_channel"); + } + + #[test] + fn parse_where_now_error_response() { + let body = WhereNowResponseBody::ErrorResponse(APIErrorBody::AsObjectWithService { + status: 400, + error: true, + service: "service".into(), + message: "error".into(), + }); + let result: Result = body.try_into(); + + assert!(result.is_err()); + } } From 42c2d65b8552383f7657baf0705aba17b23099da Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 00:49:27 +0200 Subject: [PATCH 10/14] clippy --- src/dx/presence/builders/where_now.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/dx/presence/builders/where_now.rs b/src/dx/presence/builders/where_now.rs index e0808908..661686f0 100644 --- a/src/dx/presence/builders/where_now.rs +++ b/src/dx/presence/builders/where_now.rs @@ -3,17 +3,12 @@ //! The [`WhereNowRequestBuilder`] lets you make and execute a Here Now request //! that will associate a user with a channel. -use core::ops::Deref; - use derive_builder::Builder; use crate::{ core::{ utils::{ - encoding::{ - url_encode_extended, url_encoded_channel_groups, url_encoded_channels, - UrlEncodeExtension, - }, + encoding::{url_encode_extended, UrlEncodeExtension}, headers::{APPLICATION_JSON, CONTENT_TYPE}, }, Deserializer, PubNubError, Transport, TransportMethod, TransportRequest, @@ -61,7 +56,7 @@ impl WhereNowRequestBuilder { self.user_id .clone() .is_some_and(|id| !id.is_empty()) - .then(|| ()) + .then_some(()) .ok_or_else(|| { "User ID is empty. It should be provided and not empty string.".to_owned() }) From 589bb85b9a1c4c65e196ffa406ce3bdee6a1394b Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 02:32:23 +0200 Subject: [PATCH 11/14] compiles --- examples/presence_state.rs | 45 +++-- .../presence/builders/get_presence_state.rs | 168 ++++++++++++++++ src/dx/presence/builders/mod.rs | 4 + src/dx/presence/mod.rs | 42 ++++ src/dx/presence/result.rs | 187 ++++++++++++++++++ 5 files changed, 423 insertions(+), 23 deletions(-) create mode 100644 src/dx/presence/builders/get_presence_state.rs diff --git a/examples/presence_state.rs b/examples/presence_state.rs index 63ccf3d8..95f17492 100644 --- a/examples/presence_state.rs +++ b/examples/presence_state.rs @@ -6,7 +6,7 @@ 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 +17,26 @@ 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("{\"is_doing\": \"Nothing. Just hanging around\"}") + .channels(["my_channel".into(), "other_channel".into()].to_vec()) + .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); + }); + 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..a6134c00 --- /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/{}/data", + 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..d623479c 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::{GetStateRequest, 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()); + } } From 56682cb0f21f62ed2bcc5354111da8819fe4425b Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 02:39:24 +0200 Subject: [PATCH 12/14] working --- examples/presence_state.rs | 13 ++++++++++++- src/dx/presence/builders/get_presence_state.rs | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/presence_state.rs b/examples/presence_state.rs index 95f17492..4bd2826f 100644 --- a/examples/presence_state.rs +++ b/examples/presence_state.rs @@ -1,6 +1,12 @@ 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")?; @@ -18,12 +24,17 @@ async fn main() -> Result<(), Box> { println!("running!"); client - .set_presence_state("{\"is_doing\": \"Nothing. Just hanging around\"}") + .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()) diff --git a/src/dx/presence/builders/get_presence_state.rs b/src/dx/presence/builders/get_presence_state.rs index a6134c00..e509e13c 100644 --- a/src/dx/presence/builders/get_presence_state.rs +++ b/src/dx/presence/builders/get_presence_state.rs @@ -120,7 +120,7 @@ impl GetStateRequest { Ok(TransportRequest { path: format!( - "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}/data", + "/v2/presence/sub-key/{sub_key}/channel/{}/uuid/{}", url_encoded_channels(&self.channels), url_encode_extended(self.user_id.as_bytes(), UrlEncodeExtension::NonChannelPath) ), From 12da2c0742129b45af6825a05f1ce787df13276e Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 02:40:48 +0200 Subject: [PATCH 13/14] blocking --- examples/presence_state_blocking.rs | 52 +++++++++++++++++------------ 1 file changed, 31 insertions(+), 21 deletions(-) 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(()) } From fc14cd7d17abb306edbc45af17aed9450550c649 Mon Sep 17 00:00:00 2001 From: Xavrax Date: Wed, 30 Aug 2023 02:41:17 +0200 Subject: [PATCH 14/14] clippy --- src/dx/presence/builders/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dx/presence/builders/mod.rs b/src/dx/presence/builders/mod.rs index d623479c..5e1636b0 100644 --- a/src/dx/presence/builders/mod.rs +++ b/src/dx/presence/builders/mod.rs @@ -26,7 +26,7 @@ pub(crate) use where_now::WhereNowRequestBuilder; pub(crate) mod where_now; #[doc(inline)] -pub(crate) use get_presence_state::{GetStateRequest, GetStateRequestBuilder}; +pub(crate) use get_presence_state::GetStateRequestBuilder; pub(crate) mod get_presence_state; use crate::{dx::pubnub_client::PubNubClientInstance, lib::alloc::string::String};