From a57af9e91a47d3abbbf0a42837a915990c7336c8 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 30 Nov 2024 17:58:22 +0100 Subject: [PATCH 1/3] feat(helix): add `Get User Extensions` --- Cargo.toml | 1 + src/helix/client/client_ext.rs | 15 ++ .../endpoints/users/get_user_extensions.rs | 193 ++++++++++++++++++ src/helix/endpoints/users/mod.rs | 7 +- src/helix/mod.rs | 4 +- 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 src/helix/endpoints/users/get_user_extensions.rs diff --git a/Cargo.toml b/Cargo.toml index bd1ca2296b..b830f99cc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ helix = [ "twitch_types/chat", "twitch_types/color", "twitch_types/emote", + "twitch_types/extension", "twitch_types/goal", "twitch_types/moderation", "twitch_types/points", diff --git a/src/helix/client/client_ext.rs b/src/helix/client/client_ext.rs index c7593d3330..ba154a2dbd 100644 --- a/src/helix/client/client_ext.rs +++ b/src/helix/client/client_ext.rs @@ -1343,6 +1343,21 @@ impl<'client, C: crate::HttpClient + Sync + 'client> HelixClient<'client, C> { Ok(self.req_put(req, helix::EmptyBody, token).await?.data) } + /// Gets a list of all extensions (both active and inactive) that the broadcaster has installed. + /// + /// The user ID in the access token identifies the broadcaster. + pub async fn get_user_extensions<'b, T>( + &'client self, + token: &T, + ) -> Result, ClientError> + where + T: TwitchToken + Send + Sync + ?Sized, + { + let req = helix::users::GetUserExtensionsRequest::new(); + + Ok(self.req_get(req, token).await?.data) + } + /// Retrieves the active shared chat session for a channel /// /// [`None`] is returned if no shared chat session is active. diff --git a/src/helix/endpoints/users/get_user_extensions.rs b/src/helix/endpoints/users/get_user_extensions.rs new file mode 100644 index 0000000000..4f0b89ef81 --- /dev/null +++ b/src/helix/endpoints/users/get_user_extensions.rs @@ -0,0 +1,193 @@ +//! Gets a list of all extensions (both active and inactive) that the broadcaster has installed. +//! [`get-user-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-extensions) +//! +//! The user ID in the access token identifies the broadcaster. +//! +//! ## Request: [GetUserExtensionsRequest] +//! +//! To use this endpoint, construct a [`GetUserExtensionsRequest`] with the [`GetUserExtensionsRequest::new()`] method. +//! +//! ```rust +//! use twitch_api::helix::users::get_user_extensions; +//! let request = get_user_extensions::GetUserExtensionsRequest::new(); +//! ``` +//! +//! ## Response: [Extension] +//! +//! Send the request to receive the response with [`HelixClient::req_get()`](helix::HelixClient::req_get). +//! +//! ```rust, no_run +//! use twitch_api::helix::{self, users::get_user_extensions}; +//! # use twitch_api::client; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let client: helix::HelixClient<'static, client::DummyHttpClient> = helix::HelixClient::default(); +//! # let token = twitch_oauth2::AccessToken::new("validtoken".to_string()); +//! # let token = twitch_oauth2::UserToken::from_existing(&client, token, None, None).await?; +//! let request = get_user_extensions::GetUserExtensionsRequest::new(); +//! let response: Vec = client.req_get(request, &token).await?.data; +//! # Ok(()) +//! # } +//! ``` +//! +//! You can also get the [`http::Request`] with [`request.create_request(&token, &client_id)`](helix::RequestGet::create_request) +//! and parse the [`http::Response`] with [`GetUserExtensionsRequest::parse_response(None, &request.get_uri(), response)`](GetUserExtensionsRequest::parse_response) + +use super::*; +use helix::RequestGet; + +/// Query Parameters for [Get User Extensions](super::get_user_extensions) +/// +/// [`get-user-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug, Default)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[must_use] +#[non_exhaustive] +pub struct GetUserExtensionsRequest {} + +impl GetUserExtensionsRequest { + /// Get a list of all extensions (both active and inactive) that the broadcaster has installed. + pub fn new() -> Self { Self::default() } +} + +/// Return Values for [Get User Extensions](super::get_user_extensions) +/// +/// [`get-user-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct Extension { + /// An ID that identifies the extension. + pub id: types::ExtensionId, + /// The extension's version. + pub version: String, + /// The extension's name. + pub name: String, + /// A Boolean value that determines whether the extension is configured and can be activated. + /// + /// Is true if the extension is configured and can be activated. + pub can_activate: bool, + /// The extension types that you can activate for this extension. + #[serde(rename = "type")] + pub type_: Vec, +} + +/// Where an extension can appear. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "snake_case")] +pub enum ExtensionType { + /// Displays as part of the video, taking up part of the screen. Component Extensions can be hidden by viewers. + Component, + /// Displays on mobile + Mobile, + /// Displays on top of the whole video as a transparent overlay. + Overlay, + /// Displays in a box under the video. + Panel, + /// An unknown type, contains the raw string provided by Twitch. + #[serde(untagged)] + Unknown(String), +} + +impl Request for GetUserExtensionsRequest { + type Response = Vec; + + #[cfg(feature = "twitch_oauth2")] + const OPT_SCOPE: &'static [twitch_oauth2::Scope] = + &[twitch_oauth2::Scope::UserReadBlockedUsers]; + const PATH: &'static str = "users/extensions/list"; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![any( + twitch_oauth2::Scope::UserReadBroadcast, + twitch_oauth2::Scope::UserEditBroadcast + )]; +} + +impl RequestGet for GetUserExtensionsRequest {} + +#[cfg(test)] +#[test] +fn test_request() { + use helix::*; + let req = GetUserExtensionsRequest::new(); + + let data = br#" + { + "data": [ + { + "id": "wi08ebtatdc7oj83wtl9uxwz807l8b", + "version": "1.1.8", + "name": "Streamlabs Leaderboard", + "can_activate": true, + "type": [ + "panel" + ] + }, + { + "id": "d4uvtfdr04uq6raoenvj7m86gdk16v", + "version": "2.0.2", + "name": "Prime Subscription and Loot Reminder", + "can_activate": true, + "type": [ + "overlay" + ] + }, + { + "id": "rh6jq1q334hqc2rr1qlzqbvwlfl3x0", + "version": "1.1.0", + "name": "TopClip", + "can_activate": true, + "type": [ + "mobile", + "panel" + ] + }, + { + "id": "zfh2irvx2jb4s60f02jq0ajm8vwgka", + "version": "1.0.19", + "name": "Streamlabs", + "can_activate": true, + "type": [ + "mobile", + "overlay" + ] + }, + { + "id": "lqnf3zxk0rv0g7gq92mtmnirjz2cjj", + "version": "0.0.1", + "name": "Dev Experience Test", + "can_activate": true, + "type": [ + "component", + "mobile", + "panel", + "overlay" + ] + } + ] + } + "# + .to_vec(); + + let http_response = http::Response::builder().body(data).unwrap(); + + let uri = req.get_uri().unwrap(); + assert_eq!( + uri.to_string(), + "https://api.twitch.tv/helix/users/extensions/list?" + ); + + let res = GetUserExtensionsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.len(), 5); + assert_eq!(res[4].id.as_str(), "lqnf3zxk0rv0g7gq92mtmnirjz2cjj"); + assert_eq!(res[4].version, "0.0.1"); + assert!(res[4].can_activate); + assert_eq!(res[4].type_.len(), 4); + assert_eq!(res[4].type_[0], ExtensionType::Component); + assert_eq!(res[4].type_[1], ExtensionType::Mobile); + assert_eq!(res[4].type_[2], ExtensionType::Panel); + assert_eq!(res[4].type_[3], ExtensionType::Overlay); +} diff --git a/src/helix/endpoints/users/mod.rs b/src/helix/endpoints/users/mod.rs index f9c926c0e3..2e484a7c1c 100644 --- a/src/helix/endpoints/users/mod.rs +++ b/src/helix/endpoints/users/mod.rs @@ -23,7 +23,7 @@ //! //! //! -//!
Users 🟡 5/8 +//!
Users 🟡 6/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -32,7 +32,7 @@ //! | [Get User Block List](https://dev.twitch.tv/docs/api/reference#get-user-block-list) | - | [`get_user_block_list`] | //! | [Block User](https://dev.twitch.tv/docs/api/reference#block-user) | [`HelixClient::block_user`](crate::helix::HelixClient::block_user) | [`block_user`] | //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`](crate::helix::HelixClient::unblock_user) | [`unblock_user`] | -//! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | - | - | +//! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`](crate::helix::HelixClient::get_user_extensions) | [`get_user_extensions`] | //! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | - | - | //! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | //! @@ -48,6 +48,7 @@ use std::borrow::Cow; pub mod block_user; pub mod get_user_block_list; +pub mod get_user_extensions; pub mod get_users; pub mod get_users_follows; pub mod unblock_user; @@ -58,6 +59,8 @@ pub use block_user::{BlockUser, BlockUserRequest}; #[doc(inline)] pub use get_user_block_list::{GetUserBlockListRequest, UserBlock}; #[doc(inline)] +pub use get_user_extensions::{Extension, ExtensionType, GetUserExtensionsRequest}; +#[doc(inline)] pub use get_users::{GetUsersRequest, User}; #[doc(inline)] pub use get_users_follows::{FollowRelationship, GetUsersFollowsRequest, UsersFollows}; diff --git a/src/helix/mod.rs b/src/helix/mod.rs index c803c2f51e..6769f1a455 100644 --- a/src/helix/mod.rs +++ b/src/helix/mod.rs @@ -351,7 +351,7 @@ //! //!
//! -//!
Users 🟡 5/8 +//!
Users 🟡 6/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -360,7 +360,7 @@ //! | [Get User Block List](https://dev.twitch.tv/docs/api/reference#get-user-block-list) | - | [`users::get_user_block_list`] | //! | [Block User](https://dev.twitch.tv/docs/api/reference#block-user) | [`HelixClient::block_user`] | [`users::block_user`] | //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`] | [`users::unblock_user`] | -//! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | - | - | +//! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`] | [`users::get_user_extensions`] | //! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | - | - | //! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | //! From 82f7f791f3f0cc9572bd3305be9cddb49dd0df88 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 30 Nov 2024 20:53:45 +0100 Subject: [PATCH 2/3] feat(helix): add `Get User Active Extensions` --- src/helix/client/client_ext.rs | 15 ++ .../users/get_user_active_extensions.rs | 234 ++++++++++++++++++ src/helix/endpoints/users/mod.rs | 196 ++++++++++++++- src/helix/mod.rs | 4 +- 4 files changed, 445 insertions(+), 4 deletions(-) create mode 100644 src/helix/endpoints/users/get_user_active_extensions.rs diff --git a/src/helix/client/client_ext.rs b/src/helix/client/client_ext.rs index ba154a2dbd..6fbea0efa8 100644 --- a/src/helix/client/client_ext.rs +++ b/src/helix/client/client_ext.rs @@ -1358,6 +1358,21 @@ impl<'client, C: crate::HttpClient + Sync + 'client> HelixClient<'client, C> { Ok(self.req_get(req, token).await?.data) } + /// Gets the active extensions that the broadcaster has installed for each configuration. + /// + /// The user ID in the access token identifies the broadcaster. + pub async fn get_user_active_extensions<'b, T>( + &'client self, + token: &T, + ) -> Result> + where + T: TwitchToken + Send + Sync + ?Sized, + { + let req = helix::users::GetUserActiveExtensionsRequest::new(); + + Ok(self.req_get(req, token).await?.data) + } + /// Retrieves the active shared chat session for a channel /// /// [`None`] is returned if no shared chat session is active. diff --git a/src/helix/endpoints/users/get_user_active_extensions.rs b/src/helix/endpoints/users/get_user_active_extensions.rs new file mode 100644 index 0000000000..c5340ad194 --- /dev/null +++ b/src/helix/endpoints/users/get_user_active_extensions.rs @@ -0,0 +1,234 @@ +//! Gets the active extensions that the broadcaster has installed for each configuration. +//! [`get-user-active-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) +//! +//! ## Request: [GetUserActiveExtensionsRequest] +//! +//! To use this endpoint, construct a [`GetUserActiveExtensionsRequest`] with the [`GetUserActiveExtensionsRequest::new()`] method. +//! +//! ```rust +//! use twitch_api::helix::users::get_user_active_extensions; +//! let request = +//! get_user_active_extensions::GetUserActiveExtensionsRequest::new(); +//! ``` +//! +//! ## Response: [ExtensionConfiguration] +//! +//! Send the request to receive the response with [`HelixClient::req_get()`](helix::HelixClient::req_get). +//! +//! ```rust, no_run +//! use twitch_api::helix::{self, users::get_user_active_extensions}; +//! # use twitch_api::client; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let client: helix::HelixClient<'static, client::DummyHttpClient> = helix::HelixClient::default(); +//! # let token = twitch_oauth2::AccessToken::new("validtoken".to_string()); +//! # let token = twitch_oauth2::UserToken::from_existing(&client, token, None, None).await?; +//! let request = get_user_active_extensions::GetUserActiveExtensionsRequest::new(); +//! let response: get_user_active_extensions::ExtensionConfiguration = client.req_get(request, &token).await?.data; +//! # Ok(()) +//! # } +//! ``` +//! +//! You can also get the [`http::Request`] with [`request.create_request(&token, &client_id)`](helix::RequestGet::create_request) +//! and parse the [`http::Response`] with [`GetUserActiveExtensionsRequest::parse_response(None, &request.get_uri(), response)`](GetUserActiveExtensionsRequest::parse_response) + +use std::collections::HashMap; + +use super::*; +use helix::RequestGet; +use serde::{Deserialize, Serialize}; + +/// Query Parameters for [Get User Active Extensions](super::get_user_active_extensions) +/// +/// [`get-user-active-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug, Default)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[must_use] +#[non_exhaustive] +pub struct GetUserActiveExtensionsRequest<'a> { + /// The ID of the broadcaster whose active extensions you want to get. + /// + /// This parameter is required if you specify an app access token and is optional if you specify a user access token. If you specify a user access token and don’t specify this parameter, the API uses the user ID from the access token. + #[cfg_attr(feature = "typed-builder", builder(default, setter(into)))] + #[cfg_attr(feature = "deser_borrow", serde(borrow = "'a"))] + pub user_id: Option>, +} + +impl<'a> GetUserActiveExtensionsRequest<'a> { + /// Gets the active extensions that the broadcaster has installed for each configuration. + /// + /// Requires a user access token. + pub fn new() -> Self { Self::default() } + + /// Gets the active extensions that the user has installed for each configuration. + /// + /// Requires an app access token. + pub fn user_id(user_id: impl types::IntoCow<'a, types::UserIdRef> + 'a) -> Self { + Self { + user_id: Some(user_id.into_cow()), + } + } +} + +/// Return Values for [Get User Active Extensions](super::get_user_active_extensions) +/// +/// [`get-user-active-extensions`](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ExtensionConfiguration { + /// A dictionary that contains the data for a panel extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub panel: HashMap>, + /// A dictionary that contains the data for a video-overlay extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub overlay: HashMap>, + /// A dictionary that contains the data for a video-component extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub component: HashMap>, +} + +/// An active extension slot +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ActiveExtension { + /// An ID that identifies the extension. + pub id: types::ExtensionId, + /// The extension’s version. + pub version: String, + /// The extension’s name. + pub name: String, +} + +/// An active extension slot where the extension can be positioned +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct ActivePositionedExtension { + /// An ID that identifies the extension. + pub id: types::ExtensionId, + /// The extension’s version. + pub version: String, + /// The extension’s name. + pub name: String, + /// The x-coordinate where the extension is placed. + pub x: i32, + /// The y-coordinate where the extension is placed. + pub y: i32, +} + +impl Request for GetUserActiveExtensionsRequest<'_> { + type Response = ExtensionConfiguration; + + #[cfg(feature = "twitch_oauth2")] + const OPT_SCOPE: &'static [twitch_oauth2::Scope] = + &[twitch_oauth2::Scope::UserReadBlockedUsers]; + const PATH: &'static str = "users/extensions"; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![any( + twitch_oauth2::Scope::UserReadBroadcast, + twitch_oauth2::Scope::UserEditBroadcast + )]; +} + +impl RequestGet for GetUserActiveExtensionsRequest<'_> {} + +#[cfg(test)] +#[test] +fn test_request() { + use helix::*; + let req = GetUserActiveExtensionsRequest::new(); + + let data = br#" + { + "data": { + "panel": { + "1": { + "active": true, + "id": "rh6jq1q334hqc2rr1qlzqbvwlfl3x0", + "version": "1.1.0", + "name": "TopClip" + }, + "2": { + "active": true, + "id": "wi08ebtatdc7oj83wtl9uxwz807l8b", + "version": "1.1.8", + "name": "Streamlabs Leaderboard" + }, + "3": { + "active": true, + "id": "naty2zwfp7vecaivuve8ef1hohh6bo", + "version": "1.0.9", + "name": "Streamlabs Stream Schedule & Countdown" + } + }, + "overlay": { + "1": { + "active": true, + "id": "zfh2irvx2jb4s60f02jq0ajm8vwgka", + "version": "1.0.19", + "name": "Streamlabs" + } + }, + "component": { + "1": { + "active": true, + "id": "lqnf3zxk0rv0g7gq92mtmnirjz2cjj", + "version": "0.0.1", + "name": "Dev Experience Test", + "x": 0, + "y": 0 + }, + "2": { + "active": false + } + } + } + } + "# + .to_vec(); + + let http_response = http::Response::builder().body(data).unwrap(); + + let uri = req.get_uri().unwrap(); + assert_eq!( + uri.to_string(), + "https://api.twitch.tv/helix/users/extensions?" + ); + + let res = GetUserActiveExtensionsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.panel.len(), 3); + assert_eq!(res.overlay.len(), 1); + assert_eq!(res.component.len(), 2); + + assert_eq!( + *res.overlay.get("1").unwrap(), + ExtensionSlot::Active(ActiveExtension { + id: "zfh2irvx2jb4s60f02jq0ajm8vwgka".into(), + version: "1.0.19".to_owned(), + name: "Streamlabs".to_owned(), + }) + ); + assert_eq!( + *res.component.get("1").unwrap(), + ExtensionSlot::Active(ActivePositionedExtension { + id: "lqnf3zxk0rv0g7gq92mtmnirjz2cjj".into(), + version: "0.0.1".to_owned(), + name: "Dev Experience Test".to_owned(), + x: 0, + y: 0, + }) + ); + assert_eq!(*res.component.get("2").unwrap(), ExtensionSlot::Inactive); + + assert_eq!( + res, + serde_json::from_str(&serde_json::to_string(&res).unwrap()).unwrap() + ); +} diff --git a/src/helix/endpoints/users/mod.rs b/src/helix/endpoints/users/mod.rs index 2e484a7c1c..5f777d2aeb 100644 --- a/src/helix/endpoints/users/mod.rs +++ b/src/helix/endpoints/users/mod.rs @@ -23,7 +23,7 @@ //! //! //! -//!
Users 🟡 6/8 +//!
Users 🟡 7/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -33,7 +33,7 @@ //! | [Block User](https://dev.twitch.tv/docs/api/reference#block-user) | [`HelixClient::block_user`](crate::helix::HelixClient::block_user) | [`block_user`] | //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`](crate::helix::HelixClient::unblock_user) | [`unblock_user`] | //! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`](crate::helix::HelixClient::get_user_extensions) | [`get_user_extensions`] | -//! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | - | - | +//! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | [`HelixClient::get_user_active_extensions`](crate::helix::HelixClient::get_user_active_extensions) | [`get_user_active_extensions`] | //! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | //! //!
@@ -47,6 +47,7 @@ use serde_derive::{Deserialize, Serialize}; use std::borrow::Cow; pub mod block_user; +pub mod get_user_active_extensions; pub mod get_user_block_list; pub mod get_user_extensions; pub mod get_users; @@ -57,6 +58,8 @@ pub mod update_user; #[doc(inline)] pub use block_user::{BlockUser, BlockUserRequest}; #[doc(inline)] +pub use get_user_active_extensions::{ExtensionConfiguration, GetUserActiveExtensionsRequest}; +#[doc(inline)] pub use get_user_block_list::{GetUserBlockListRequest, UserBlock}; #[doc(inline)] pub use get_user_extensions::{Extension, ExtensionType, GetUserExtensionsRequest}; @@ -68,3 +71,192 @@ pub use get_users_follows::{FollowRelationship, GetUsersFollowsRequest, UsersFol pub use unblock_user::{UnblockUser, UnblockUserRequest}; #[doc(inline)] pub use update_user::UpdateUserRequest; + +/// A slot for an extension to be active in +#[derive(PartialEq, Eq, Debug, Clone)] +#[non_exhaustive] +pub enum ExtensionSlot { + /// The slot is not configured + Inactive, + /// The slot is configured + Active(T), +} + +impl serde::Serialize for ExtensionSlot { + fn serialize(&self, ser: S) -> Result { + match self { + ExtensionSlot::Inactive => { + let mut state = serde::Serializer::serialize_struct(ser, "ExtensionSlot", 1)?; + serde::ser::SerializeStruct::serialize_field(&mut state, "active", &false)?; + serde::ser::SerializeStruct::end(state) + } + ExtensionSlot::Active(it) => it.serialize(ActiveTaggedSerializer { delegate: ser }), + } + } +} + +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for ExtensionSlot { + fn deserialize(deserializer: D) -> Result, D::Error> + where D: serde::de::Deserializer<'de> { + let mut map = serde_json::Map::deserialize(deserializer)?; + + let active = map + .remove("active") + .ok_or_else(|| serde::de::Error::missing_field("active")) + .map(serde::Deserialize::deserialize)? + .map_err(serde::de::Error::custom)?; + let rest = serde_json::Value::Object(map); + + if active { + T::deserialize(rest) + .map(Self::Active) + .map_err(serde::de::Error::custom) + } else { + Ok(Self::Inactive) + } + } +} + +/// Serializes a struct with an additional `"active": true` (used for active extensions). +/// +/// Modelled after serde's internal TaggedSerializer +struct ActiveTaggedSerializer { + delegate: S, +} + +impl ActiveTaggedSerializer { + fn bad_type(self) -> Result { + Err(serde::ser::Error::custom( + "cannot serialize with anything other than a struct", + )) + } +} + +impl serde::Serializer for ActiveTaggedSerializer { + type Error = S::Error; + type Ok = S::Ok; + type SerializeMap = serde::ser::Impossible; + type SerializeSeq = serde::ser::Impossible; + type SerializeStruct = S::SerializeStruct; + type SerializeStructVariant = serde::ser::Impossible; + type SerializeTuple = serde::ser::Impossible; + type SerializeTupleStruct = serde::ser::Impossible; + type SerializeTupleVariant = serde::ser::Impossible; + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + let mut state = self.delegate.serialize_struct(name, len + 1)?; + serde::ser::SerializeStruct::serialize_field(&mut state, "active", &true)?; + Ok(state) + } + + fn serialize_bool(self, _: bool) -> Result { self.bad_type() } + + fn serialize_i8(self, _: i8) -> Result { self.bad_type() } + + fn serialize_i16(self, _: i16) -> Result { self.bad_type() } + + fn serialize_i32(self, _: i32) -> Result { self.bad_type() } + + fn serialize_i64(self, _: i64) -> Result { self.bad_type() } + + fn serialize_u8(self, _: u8) -> Result { self.bad_type() } + + fn serialize_u16(self, _: u16) -> Result { self.bad_type() } + + fn serialize_u32(self, _: u32) -> Result { self.bad_type() } + + fn serialize_u64(self, _: u64) -> Result { self.bad_type() } + + fn serialize_f32(self, _: f32) -> Result { self.bad_type() } + + fn serialize_f64(self, _: f64) -> Result { self.bad_type() } + + fn serialize_char(self, _: char) -> Result { self.bad_type() } + + fn serialize_str(self, _: &str) -> Result { self.bad_type() } + + fn serialize_bytes(self, _: &[u8]) -> Result { self.bad_type() } + + fn serialize_none(self) -> Result { self.bad_type() } + + fn serialize_some(self, _: &T) -> Result { + self.bad_type() + } + + fn serialize_unit(self) -> Result { self.bad_type() } + + fn serialize_unit_struct(self, _: &'static str) -> Result { + self.bad_type() + } + + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + ) -> Result { + self.bad_type() + } + + fn serialize_newtype_struct( + self, + _: &'static str, + _: &T, + ) -> Result { + self.bad_type() + } + + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: &T, + ) -> Result { + self.bad_type() + } + + fn serialize_seq(self, _: Option) -> Result { + self.bad_type() + } + + fn serialize_tuple(self, _: usize) -> Result { + self.bad_type() + } + + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + self.bad_type() + } + + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + self.bad_type() + } + + fn serialize_map(self, _: Option) -> Result { + self.bad_type() + } + + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + _: &'static str, + _: usize, + ) -> Result { + self.bad_type() + } +} diff --git a/src/helix/mod.rs b/src/helix/mod.rs index 6769f1a455..96c17dd9da 100644 --- a/src/helix/mod.rs +++ b/src/helix/mod.rs @@ -351,7 +351,7 @@ //! //!
//! -//!
Users 🟡 6/8 +//!
Users 🟡 7/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -361,7 +361,7 @@ //! | [Block User](https://dev.twitch.tv/docs/api/reference#block-user) | [`HelixClient::block_user`] | [`users::block_user`] | //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`] | [`users::unblock_user`] | //! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`] | [`users::get_user_extensions`] | -//! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | - | - | +//! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | [`HelixClient::get_user_active_extensions`] | [`users::get_user_active_extensions`] | //! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | //! //!
From 47950a55674a50aa9bc44cbe7cf22e5c5924a05d Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 1 Dec 2024 09:28:50 +0100 Subject: [PATCH 3/3] feat(helix): add `Update User Extensions` --- src/helix/endpoints/users/mod.rs | 9 +- .../endpoints/users/update_user_extensions.rs | 379 ++++++++++++++++++ src/helix/mod.rs | 4 +- 3 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 src/helix/endpoints/users/update_user_extensions.rs diff --git a/src/helix/endpoints/users/mod.rs b/src/helix/endpoints/users/mod.rs index 5f777d2aeb..95419fa6b3 100644 --- a/src/helix/endpoints/users/mod.rs +++ b/src/helix/endpoints/users/mod.rs @@ -23,7 +23,7 @@ //! //! //! -//!
Users 🟡 7/8 +//!
Users 🟢 8/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -34,7 +34,7 @@ //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`](crate::helix::HelixClient::unblock_user) | [`unblock_user`] | //! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`](crate::helix::HelixClient::get_user_extensions) | [`get_user_extensions`] | //! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | [`HelixClient::get_user_active_extensions`](crate::helix::HelixClient::get_user_active_extensions) | [`get_user_active_extensions`] | -//! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | +//! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | [`update_user_extensions`] | //! //!
//! @@ -54,6 +54,7 @@ pub mod get_users; pub mod get_users_follows; pub mod unblock_user; pub mod update_user; +pub mod update_user_extensions; #[doc(inline)] pub use block_user::{BlockUser, BlockUserRequest}; @@ -71,6 +72,10 @@ pub use get_users_follows::{FollowRelationship, GetUsersFollowsRequest, UsersFol pub use unblock_user::{UnblockUser, UnblockUserRequest}; #[doc(inline)] pub use update_user::UpdateUserRequest; +#[doc(inline)] +pub use update_user_extensions::{ + ExtensionSpecification, UpdateUserExtensionsBody, UpdateUserExtensionsRequest, +}; /// A slot for an extension to be active in #[derive(PartialEq, Eq, Debug, Clone)] diff --git a/src/helix/endpoints/users/update_user_extensions.rs b/src/helix/endpoints/users/update_user_extensions.rs new file mode 100644 index 0000000000..83fb94d74f --- /dev/null +++ b/src/helix/endpoints/users/update_user_extensions.rs @@ -0,0 +1,379 @@ +//! Updates the specified user’s information. +//! +//! [`update-user-extensions`](https://dev.twitch.tv/docs/api/reference/#update-user-extensions) +//! +//! The user ID in the OAuth token identifies the user whose information you want to update. +//! +//! # Accessing the endpoint +//! +//! ## Request: [UpdateUserExtensionsRequest] +//! +//! To use this endpoint, construct an [`UpdateUserExtensionsRequest`] with the [`UpdateUserExtensionsRequest::new()`] method. +//! +//! ```rust +//! use twitch_api::helix::users::update_user_extensions; +//! let mut request = +//! update_user_extensions::UpdateUserExtensionsRequest::new(); +//! ``` +//! +//! ## Body: [UpdateUserExtensionsBody] +//! +//! We also need to provide a body to the request. +//! +//! ``` +//! # use twitch_api::helix::users::{self, update_user_extensions}; +//! # use std::collections::HashMap; +//! # use std::iter::FromIterator; +//! # use std::borrow::Cow; +//! let body = update_user_extensions::UpdateUserExtensionsBody::new( +//! update_user_extensions::ExtensionSpecification::new().panel( +//! HashMap::from_iter([( +//! Cow::Borrowed("1"), +//! users::ExtensionSlot::Inactive, +//! )]), +//! ), +//! ); +//! ``` +//! +//! ## Response: [ExtensionConfiguration] +//! +//! +//! Send the request to receive the response with [`HelixClient::req_patch()`](helix::HelixClient::req_patch). +//! +//! +//! ```rust, no_run +//! use twitch_api::helix::{self, users::update_user_extensions}; +//! # use twitch_api::client; +//! # use std::{iter::FromIterator, borrow::Cow, collections::HashMap}; +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # let client: helix::HelixClient<'static, client::DummyHttpClient> = helix::HelixClient::default(); +//! # let token = twitch_oauth2::AccessToken::new("validtoken".to_string()); +//! # let token = twitch_oauth2::UserToken::from_existing(&client, token, None, None).await?; +//! let mut request = update_user_extensions::UpdateUserExtensionsRequest::new(); +//! let body = update_user_extensions::UpdateUserExtensionsBody::new( +//! update_user_extensions::ExtensionSpecification::new() +//! .panel(HashMap::from_iter([(Cow::Borrowed("1"), helix::users::ExtensionSlot::Inactive)])) +//! ); +//! let response: helix::users::ExtensionConfiguration = client.req_put(request, body, &token).await?.data; +//! # Ok(()) +//! # } +//! ``` +//! +//! You can also get the [`http::Request`] with [`request.create_request(&token, &client_id)`](helix::RequestPost::create_request) +//! and parse the [`http::Response`] with [`UpdateUserExtensionsRequest::parse_response(None, &request.get_uri(), response)`](UpdateUserExtensionsRequest::parse_response) +use std::collections::HashMap; + +use super::*; +use helix::RequestPut; + +/// Query Parameters for [Update User Extensions](super::update_user_extensions) +/// +/// [`update-user-extensions`](https://dev.twitch.tv/docs/api/reference#update-user-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug, Default)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct UpdateUserExtensionsRequest<'a> { + #[serde(skip)] + _phantom: std::marker::PhantomData<&'a ()>, +} + +impl UpdateUserExtensionsRequest<'_> { + /// Create a new update request + pub fn new() -> Self { Self::default() } +} + +impl Request for UpdateUserExtensionsRequest<'_> { + type Response = ExtensionConfiguration; + + const PATH: &'static str = "users/extensions"; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = + twitch_oauth2::validator![twitch_oauth2::Scope::UserEditBroadcast]; +} + +/// Body for [Update User Extensions](super::update_user_extensions) +/// +/// [`update-user-extensions`](https://dev.twitch.tv/docs/api/reference#update-user-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct UpdateUserExtensionsBody<'a> { + /// The specification for the user's extensions (which extensions to update) + pub data: ExtensionSpecification<'a>, +} + +impl helix::private::SealedSerialize for UpdateUserExtensionsBody<'_> {} + +impl<'a> UpdateUserExtensionsBody<'a> { + /// Create a new specificaton for the user's extensions + pub fn new(data: ExtensionSpecification<'a>) -> Self { Self { data } } +} + +/// Inner body for [Update User Extensions](super::update_user_extensions) +/// +/// [`update-user-extensions`](https://dev.twitch.tv/docs/api/reference#update-user-extensions) +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone, Default)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct ExtensionSpecification<'a> { + /// A dictionary that contains the data for a panel extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub panel: Option, ExtensionSlot>>>, + /// A dictionary that contains the data for a video-overlay extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub overlay: Option, ExtensionSlot>>>, + /// A dictionary that contains the data for a video-component extension. + /// + /// The dictionary’s key is a sequential number beginning with 1. + pub component: Option, ExtensionSlot>>>, +} + +impl<'a> ExtensionSpecification<'a> { + /// Create an empty specification + pub fn new() -> Self { Self::default() } + + /// Set the panel extensions + pub fn panel( + mut self, + panel: HashMap, ExtensionSlot>>, + ) -> Self { + self.panel = Some(panel); + self + } + + /// Set the overlay extensions + pub fn overlay( + mut self, + overlay: HashMap, ExtensionSlot>>, + ) -> Self { + self.overlay = Some(overlay); + self + } + + /// Set the component extensions + pub fn component( + mut self, + component: HashMap, ExtensionSlot>>, + ) -> Self { + self.component = Some(component); + self + } +} + +/// An active extension slot +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct ActiveExtension<'a> { + /// An ID that identifies the extension. + pub id: Cow<'a, types::ExtensionIdRef>, + /// The extension’s version. + pub version: Cow<'a, str>, +} + +impl<'a> ActiveExtension<'a> { + /// Create an active extension with an ID and a version + pub fn new( + id: impl types::IntoCow<'a, types::ExtensionIdRef> + 'a, + version: impl Into>, + ) -> Self { + Self { + id: id.into_cow(), + version: version.into(), + } + } +} + +/// An active extension slot where the extension can be positioned +#[derive(PartialEq, Eq, Deserialize, Serialize, Debug, Clone)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct ActivePositionedExtension<'a> { + /// An ID that identifies the extension. + pub id: Cow<'a, types::ExtensionIdRef>, + /// The extension’s version. + pub version: Cow<'a, str>, + /// The x-coordinate where the extension is placed. + pub x: i32, + /// The y-coordinate where the extension is placed. + pub y: i32, +} + +impl<'a> ActivePositionedExtension<'a> { + /// Create an active positioned extension with an ID, a version, and a position + pub fn new( + id: impl types::IntoCow<'a, types::ExtensionIdRef> + 'a, + version: impl Into>, + x: i32, + y: i32, + ) -> Self { + Self { + id: id.into_cow(), + version: version.into(), + x, + y, + } + } +} + +impl<'a> RequestPut for UpdateUserExtensionsRequest<'a> { + type Body = UpdateUserExtensionsBody<'a>; + + fn parse_inner_response( + request: Option, + uri: &http::Uri, + response: &str, + status: http::StatusCode, + ) -> Result::Response>, helix::HelixRequestPutError> + where + Self: Sized, + { + let inner_response: helix::InnerResponse<::Response> = + crate::parse_json(response, true).map_err(|e| { + helix::HelixRequestPutError::DeserializeError( + response.to_string(), + e, + uri.clone(), + status, + ) + })?; + Ok(helix::Response::new( + inner_response.data, + inner_response.pagination.cursor, + request, + inner_response.total, + inner_response.other, + )) + } +} + +#[cfg(test)] +#[test] +fn test_request() { + use helix::*; + use std::iter::FromIterator; + + let req = UpdateUserExtensionsRequest::new(); + + let spec = ExtensionSpecification::new() + .panel(HashMap::from_iter([ + ( + Cow::Borrowed("1"), + ExtensionSlot::Active(ActiveExtension::new( + "rh6jq1q334hqc2rr1qlzqbvwlfl3x0", + "1.1.0", + )), + ), + ( + Cow::Borrowed("2"), + ExtensionSlot::Active(ActiveExtension::new( + "wi08ebtatdc7oj83wtl9uxwz807l8b", + "1.1.8", + )), + ), + ( + Cow::Borrowed("3"), + ExtensionSlot::Active(ActiveExtension::new( + "naty2zwfp7vecaivuve8ef1hohh6bo", + "1.0.9", + )), + ), + ])) + .overlay(HashMap::from_iter([( + Cow::Borrowed("1"), + ExtensionSlot::Active(ActiveExtension::new( + "zfh2irvx2jb4s60f02jq0ajm8vwgka", + "1.0.19", + )), + )])) + .component(HashMap::from_iter([ + ( + Cow::Borrowed("1"), + ExtensionSlot::Active(ActivePositionedExtension::new( + "lqnf3zxk0rv0g7gq92mtmnirjz2cjj", + "0.0.1", + 0, + 0, + )), + ), + (Cow::Borrowed("2"), ExtensionSlot::Inactive), + ])); + let body = UpdateUserExtensionsBody::new(spec); + + assert_eq!( + body, + serde_json::from_str(&serde_json::to_string(&body).unwrap()).unwrap() + ); + + // XXX: can't test the serialized body as the order of HashMap is unspecified + + dbg!(req.create_request(body, "token", "clientid").unwrap()); + + let data = br#" + { + "data": { + "panel": { + "1": { + "active": true, + "id": "rh6jq1q334hqc2rr1qlzqbvwlfl3x0", + "version": "1.1.0", + "name": "TopClip" + }, + "2": { + "active": true, + "id": "wi08ebtatdc7oj83wtl9uxwz807l8b", + "version": "1.1.8", + "name": "Streamlabs Leaderboard" + }, + "3": { + "active": true, + "id": "naty2zwfp7vecaivuve8ef1hohh6bo", + "version": "1.0.9", + "name": "Streamlabs Stream Schedule & Countdown" + } + }, + "overlay": { + "1": { + "active": true, + "id": "zfh2irvx2jb4s60f02jq0ajm8vwgka", + "version": "1.0.19", + "name": "Streamlabs" + } + }, + "component": { + "1": { + "active": true, + "id": "lqnf3zxk0rv0g7gq92mtmnirjz2cjj", + "version": "0.0.1", + "name": "Dev Experience Test", + "x": 0, + "y": 0 + }, + "2": { + "active": false + } + } + } + } + "# + .to_vec(); + + let http_response = http::Response::builder().body(data).unwrap(); + + let uri = req.get_uri().unwrap(); + assert_eq!( + uri.to_string(), + "https://api.twitch.tv/helix/users/extensions?" + ); + + let res = UpdateUserExtensionsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.panel.len(), 3); + assert_eq!(res.overlay.len(), 1); + assert_eq!(res.component.len(), 2); + assert_eq!(*res.component.get("2").unwrap(), ExtensionSlot::Inactive); +} diff --git a/src/helix/mod.rs b/src/helix/mod.rs index 96c17dd9da..688acf7c1b 100644 --- a/src/helix/mod.rs +++ b/src/helix/mod.rs @@ -351,7 +351,7 @@ //! //!
//! -//!
Users 🟡 7/8 +//!
Users 🟢 8/8 //! //! | Endpoint | Helper | Module | //! |---|---|---| @@ -362,7 +362,7 @@ //! | [Unblock User](https://dev.twitch.tv/docs/api/reference#unblock-user) | [`HelixClient::unblock_user`] | [`users::unblock_user`] | //! | [Get User Extensions](https://dev.twitch.tv/docs/api/reference#get-user-extensions) | [`HelixClient::get_user_extensions`] | [`users::get_user_extensions`] | //! | [Get User Active Extensions](https://dev.twitch.tv/docs/api/reference#get-user-active-extensions) | [`HelixClient::get_user_active_extensions`] | [`users::get_user_active_extensions`] | -//! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | - | +//! | [Update User Extensions](https://dev.twitch.tv/docs/api/reference#update-user-extensions) | - | [`users::update_user_extensions`] | //! //!
//!