From f491a7f8e1a53be1e6ac9d33712c107cb316aca8 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 12 Oct 2024 13:52:29 +0200 Subject: [PATCH 1/2] feat: add `Get AutoMod Settings` endpoint --- CHANGELOG.md | 1 + .../moderation/get_automod_settings.rs | 214 ++++++++++++++++++ src/helix/endpoints/moderation/mod.rs | 3 + src/helix/mod.rs | 8 +- src/helix/request.rs | 38 ++++ 5 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 src/helix/endpoints/moderation/get_automod_settings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4329b0ceac..6cd9cb9122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ - Added beta `channel.ad_break.begin` eventsub event - Added `conduit.shard.disable` EventSub event - Added `title` and `description` as fields in the response of `Get Channel Chat Badges` and `Get Global Chat Badges` +- Added `Get AutoMod Settings` endpoint ### Fixed diff --git a/src/helix/endpoints/moderation/get_automod_settings.rs b/src/helix/endpoints/moderation/get_automod_settings.rs new file mode 100644 index 0000000000..0b179507a4 --- /dev/null +++ b/src/helix/endpoints/moderation/get_automod_settings.rs @@ -0,0 +1,214 @@ +//! Gets the broadcaster’s AutoMod settings. +//! [`get-automod-settings`](https://dev.twitch.tv/docs/api/reference/#get-automod-settings) +//! +//! # Accessing the endpoint +//! +//! ## Request: [GetAutoModSettingsRequest] +//! +//! To use this endpoint, construct a [`GetAutoModSettingsRequest`] with the [`GetAutoModSettingsRequest::new()`] method. +//! +//! ```rust +//! use twitch_api::helix::moderation::get_automod_settings; +//! let request = +//! get_automod_settings::GetAutoModSettingsRequest::new("1234", "5678"); +//! ``` +//! +//! ## Response: [AutoModSettings] +//! +//! Send the request to receive the response with [`HelixClient::req_get()`](helix::HelixClient::req_get). +//! +//! ```rust, no_run +//! use twitch_api::helix::{self, moderation::get_automod_settings}; +//! # 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_automod_settings::GetAutoModSettingsRequest::new("1234", "5678"); +//! let response: helix::moderation::AutoModSettings = 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 [`GetAutoModSettingsRequest::parse_response(None, &request.get_uri(), response)`](GetAutoModSettingsRequest::parse_response) + +use super::*; +use helix::RequestGet; + +/// Query Parameters for [Get AutoMod Settings](super::get_automod_settings) +/// +/// [`get-automod-settings`](https://dev.twitch.tv/docs/api/reference/#get-automod-settings) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[must_use] +#[non_exhaustive] +pub struct GetAutoModSettingsRequest<'a> { + /// The ID of the broadcaster whose AutoMod settings you want to get. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + #[cfg_attr(feature = "deser_borrow", serde(borrow = "'a"))] + pub broadcaster_id: Cow<'a, types::UserIdRef>, + /// The ID of the broadcaster or a user that has permission to moderate the broadcaster’s chat room. This ID must match the user ID in the user access token. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + #[cfg_attr(feature = "deser_borrow", serde(borrow = "'a"))] + pub moderator_id: Cow<'a, types::UserIdRef>, +} + +impl<'a> GetAutoModSettingsRequest<'a> { + /// Get AutoMod settings in a broadcasters channel as specified moderator + pub fn new( + broadcaster_id: impl types::IntoCow<'a, types::UserIdRef> + 'a, + moderator_id: impl types::IntoCow<'a, types::UserIdRef> + 'a, + ) -> Self { + Self { + broadcaster_id: broadcaster_id.into_cow(), + moderator_id: moderator_id.into_cow(), + } + } +} + +/// Return Values for [Get AutoMod Settings](super::get_automod_settings) +/// +/// [`get-automod-settings`](https://dev.twitch.tv/docs/api/reference/#get-automod-settings) +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "deny_unknown_fields", serde(deny_unknown_fields))] +#[non_exhaustive] +pub struct AutoModSettings { + /// The broadcaster’s ID. + pub broadcaster_id: types::UserId, + /// The moderator’s ID. + pub moderator_id: types::UserId, + /// The default AutoMod level for the broadcaster. This field is [None] if the broadcaster has set one or more of the individual settings. + pub overall_level: Option, + /// The Automod level for discrimination against disability. + pub disability: u8, + /// The Automod level for hostility involving aggression. + pub aggression: u8, + /// The AutoMod level for discrimination based on sexuality, sex, or gender. + pub sexuality_sex_or_gender: u8, + /// The Automod level for discrimination against women. + pub misogyny: u8, + /// The Automod level for hostility involving name calling or insults. + pub bullying: u8, + /// The Automod level for profanity. + pub swearing: u8, + /// The Automod level for racial discrimination. + pub race_ethnicity_or_religion: u8, + /// The Automod level for sexual content. + pub sex_based_terms: u8, +} + +impl Request for GetAutoModSettingsRequest<'_> { + type Response = AutoModSettings; + + const PATH: &'static str = "moderation/automod/settings"; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = twitch_oauth2::validator![any( + twitch_oauth2::Scope::ModeratorReadAutomodSettings, + twitch_oauth2::Scope::ModeratorManageAutomodSettings + )]; +} + +impl RequestGet for GetAutoModSettingsRequest<'_> { + fn parse_inner_response( + request: Option, + uri: &http::Uri, + response: &str, + status: http::StatusCode, + ) -> Result::Response>, helix::HelixRequestGetError> + where + Self: Sized, + { + helix::parse_single_return(request, uri, response, status) + } +} + +#[cfg(test)] +#[test] +fn test_request() { + use helix::*; + let req = GetAutoModSettingsRequest::new("1234", "5678"); + + let data = br#" + { + "data": [ + { + "broadcaster_id": "1234", + "moderator_id": "5678", + "overall_level": null, + "disability": 0, + "aggression": 0, + "sexuality_sex_or_gender": 0, + "misogyny": 0, + "bullying": 0, + "swearing": 0, + "race_ethnicity_or_religion": 0, + "sex_based_terms": 0 + } + ] + } +"# + .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/moderation/automod/settings?broadcaster_id=1234&moderator_id=5678" + ); + + let res = GetAutoModSettingsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.overall_level, None); + assert_eq!(res.disability, 0); + assert_eq!(res.broadcaster_id.as_str(), "1234"); + assert_eq!(res.moderator_id.as_str(), "5678"); +} + +#[cfg(test)] +#[test] +fn test_request_with_overall() { + use helix::*; + let req = GetAutoModSettingsRequest::new("1234", "5678"); + + let data = br#" + { + "data": [ + { + "aggression": 1, + "broadcaster_id": "1234", + "bullying": 0, + "disability": 0, + "misogyny": 0, + "moderator_id": "5678", + "overall_level": 1, + "race_ethnicity_or_religion": 1, + "sex_based_terms": 0, + "sexuality_sex_or_gender": 1, + "swearing": 0 + } + ] + } +"# + .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/moderation/automod/settings?broadcaster_id=1234&moderator_id=5678" + ); + + let res = GetAutoModSettingsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.overall_level, Some(1)); + assert_eq!(res.aggression, 1); + assert_eq!(res.disability, 0); + assert_eq!(res.broadcaster_id.as_str(), "1234"); + assert_eq!(res.moderator_id.as_str(), "5678"); +} diff --git a/src/helix/endpoints/moderation/mod.rs b/src/helix/endpoints/moderation/mod.rs index 164bb23d40..d8e1a3e829 100644 --- a/src/helix/endpoints/moderation/mod.rs +++ b/src/helix/endpoints/moderation/mod.rs @@ -13,6 +13,7 @@ pub mod add_channel_moderator; pub mod ban_user; pub mod check_automod_status; pub mod delete_chat_messages; +pub mod get_automod_settings; pub mod get_banned_users; pub mod get_blocked_terms; pub mod get_moderators; @@ -38,6 +39,8 @@ pub use check_automod_status::{ #[doc(inline)] pub use delete_chat_messages::{DeleteChatMessagesRequest, DeleteChatMessagesResponse}; #[doc(inline)] +pub use get_automod_settings::{AutoModSettings, GetAutoModSettingsRequest}; +#[doc(inline)] pub use get_banned_users::{BannedUser, GetBannedUsersRequest}; #[doc(inline)] pub use get_moderators::{GetModeratorsRequest, Moderator}; diff --git a/src/helix/mod.rs b/src/helix/mod.rs index 0eecbb9fef..defe3eeb62 100644 --- a/src/helix/mod.rs +++ b/src/helix/mod.rs @@ -231,13 +231,13 @@ //! //! //! -//!
Moderation 🟡 18/23 +//!
Moderation 🟡 19/23 //! //! | Endpoint | Helper | Module | //! |---|---|---| //! | [Check AutoMod Status](https://dev.twitch.tv/docs/api/reference#check-automod-status) | - | [`moderation::check_automod_status`] | //! | [Manage Held AutoMod Messages](https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages) | - | [`moderation::manage_held_automod_messages`] | -//! | [Get AutoMod Settings](https://dev.twitch.tv/docs/api/reference#get-automod-settings) | - | - | +//! | [Get AutoMod Settings](https://dev.twitch.tv/docs/api/reference#get-automod-settings) | - | [`moderation::get_automod_settings`] | //! | [Update AutoMod Settings](https://dev.twitch.tv/docs/api/reference#update-automod-settings) | - | - | //! | [Get Banned Users](https://dev.twitch.tv/docs/api/reference#get-banned-users) | - | [`moderation::get_banned_users`] | //! | [Ban User](https://dev.twitch.tv/docs/api/reference#ban-user) | [`HelixClient::ban_user`] | [`moderation::ban_user`] | @@ -411,7 +411,9 @@ pub use request::errors::{ HelixRequestPostError, HelixRequestPutError, InvalidUri, SerializeError, }; #[doc(inline)] -pub use request::{Request, RequestDelete, RequestGet, RequestPatch, RequestPost, RequestPut}; +pub use request::{ + parse_single_return, Request, RequestDelete, RequestGet, RequestPatch, RequestPost, RequestPut, +}; #[doc(inline)] pub use response::Response; diff --git a/src/helix/request.rs b/src/helix/request.rs index d70d5c806c..2dfb662e3b 100644 --- a/src/helix/request.rs +++ b/src/helix/request.rs @@ -436,3 +436,41 @@ pub trait RequestGet: Request { )) } } + +/// Parses a response where Helix responds with a single datum inside `data`. +/// +/// An example response is `{ "data": [ { "foo": 1 } ]`. +pub fn parse_single_return( + request: Option, + uri: &http::Uri, + response: &str, + status: http::StatusCode, +) -> Result, HelixRequestGetError> +where + T: RequestGet, +{ + let resp = match status { + http::StatusCode::OK => { + let resp: InnerResponse<[T::Response; 1]> = + parse_json(response, true).map_err(|e| { + HelixRequestGetError::DeserializeError( + response.to_string(), + e, + uri.clone(), + status, + ) + })?; + let [s] = resp.data; + s + } + _ => { + return Err(HelixRequestGetError::InvalidResponse { + reason: "unexpected status code", + response: response.to_string(), + status, + uri: uri.clone(), + }) + } + }; + Ok(Response::with_data(resp, request)) +} From ca2f5708c60073a95131cd864776a0e493d5b19a Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 12 Oct 2024 15:35:20 +0200 Subject: [PATCH 2/2] feat: add `Update AutoMod Settings` endpoint --- CHANGELOG.md | 2 +- src/helix/endpoints/moderation/mod.rs | 5 + .../moderation/update_automod_settings.rs | 322 ++++++++++++++++++ src/helix/mod.rs | 9 +- src/helix/request.rs | 28 +- src/helix/request/errors.rs | 70 ++++ 6 files changed, 413 insertions(+), 23 deletions(-) create mode 100644 src/helix/endpoints/moderation/update_automod_settings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd9cb9122..c7a816c5a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,7 +79,7 @@ - Added beta `channel.ad_break.begin` eventsub event - Added `conduit.shard.disable` EventSub event - Added `title` and `description` as fields in the response of `Get Channel Chat Badges` and `Get Global Chat Badges` -- Added `Get AutoMod Settings` endpoint +- Added `Get AutoMod Settings` and `Update AutoMod Settings` endpoints ### Fixed diff --git a/src/helix/endpoints/moderation/mod.rs b/src/helix/endpoints/moderation/mod.rs index d8e1a3e829..94ac738bff 100644 --- a/src/helix/endpoints/moderation/mod.rs +++ b/src/helix/endpoints/moderation/mod.rs @@ -22,6 +22,7 @@ pub mod manage_held_automod_messages; pub mod remove_blocked_term; pub mod remove_channel_moderator; pub mod unban_user; +pub mod update_automod_settings; pub mod update_shield_mode_status; #[cfg(feature = "beta")] pub mod warn_chat_user; @@ -57,6 +58,10 @@ pub use remove_blocked_term::{RemoveBlockedTerm, RemoveBlockedTermRequest}; pub use remove_channel_moderator::{RemoveChannelModeratorRequest, RemoveChannelModeratorResponse}; #[doc(inline)] pub use unban_user::{UnbanUserRequest, UnbanUserResponse}; +#[doc(inline)] +pub use update_automod_settings::{ + UpdateAutoModSettingsBody, UpdateAutoModSettingsIndividual, UpdateAutoModSettingsRequest, +}; #[cfg(feature = "beta")] #[doc(inline)] pub use warn_chat_user::{WarnChatUser, WarnChatUserBody, WarnChatUserRequest}; diff --git a/src/helix/endpoints/moderation/update_automod_settings.rs b/src/helix/endpoints/moderation/update_automod_settings.rs new file mode 100644 index 0000000000..41ab98bf3f --- /dev/null +++ b/src/helix/endpoints/moderation/update_automod_settings.rs @@ -0,0 +1,322 @@ +//! Updates the broadcaster’s AutoMod settings. +//! +//! [`update-automod-settings`](https://dev.twitch.tv/docs/api/reference#update-automod-settings) +//! The settings are used to automatically block inappropriate or harassing messages from appearing in the broadcaster’s chat room. +//! +//! # Accessing the endpoint +//! +//! ## Request: [UpdateAutoModSettingsRequest] +//! +//! To use this endpoint, construct an [`UpdateAutoModSettingsRequest`] with the [`UpdateAutoModSettingsRequest::new()`] method. +//! +//! ```rust +//! use twitch_api::helix::moderation::update_automod_settings; +//! let request = update_automod_settings::UpdateAutoModSettingsRequest::new( +//! "123", "456", +//! ); +//! ``` +//! +//! ## Body: [UpdateAutoModSettingsBody] +//! +//! We also need to provide a body to the request. +//! +//! ``` +//! # use twitch_api::helix::moderation::update_automod_settings; +//! // Set the overall level to 3 +//! let body = update_automod_settings::UpdateAutoModSettingsBody::overall(3); +//! ``` +//! +//! +//! ## Response: [AutoModSettings] +//! +//! Send the request to receive the response with [`HelixClient::req_patch()`](helix::HelixClient::req_patch). +//! +//! ```rust, no_run +//! use twitch_api::helix::{self, moderation::update_automod_settings}; +//! # 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 = update_automod_settings::UpdateAutoModSettingsRequest::new( +//! "123", +//! "456" +//! ); +//! let body = +//! update_automod_settings::UpdateAutoModSettingsBody::overall(3); +//! let response: helix::moderation::AutoModSettings = client.req_put(request, body, &token).await?.data; +//! # Ok(()) +//! # } +//! ``` +//! +//! You can also get the [`http::Request`] with [`request.create_request(&token, &client_id)`](helix::RequestPut::create_request) +//! and parse the [`http::Response`] with [`UpdateAutoModSettingsRequest::parse_response(None, &request.get_uri(), response)`](UpdateAutoModSettingsRequest::parse_response) + +use super::*; +use helix::RequestPut; + +pub use super::AutoModSettings; + +/// Query Parameters for [Update AutoMod Settings](super::update_automod_settings) +/// +/// [`update-automod-settings`](https://dev.twitch.tv/docs/api/reference#update-automod-settings) +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct UpdateAutoModSettingsRequest<'a> { + /// The ID of the broadcaster whose AutoMod settings you want to update. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + #[cfg_attr(feature = "deser_borrow", serde(borrow = "'a"))] + #[cfg_attr(not(feature = "deser_borrow"), serde(bound(deserialize = "'de: 'a")))] + pub broadcaster_id: Cow<'a, types::UserIdRef>, + /// The ID of the broadcaster or a user that has permission to moderate the broadcaster’s chat room. This ID must match the user ID in the user access token. + #[cfg_attr(feature = "typed-builder", builder(setter(into)))] + #[cfg_attr(feature = "deser_borrow", serde(borrow = "'a"))] + #[cfg_attr(not(feature = "deser_borrow"), serde(bound(deserialize = "'de: 'a")))] + pub moderator_id: Cow<'a, types::UserIdRef>, +} + +impl<'a> UpdateAutoModSettingsRequest<'a> { + /// Update the AutoMod settings on the specified channel as the specified moderator + pub fn new( + broadcaster_id: impl types::IntoCow<'a, types::UserIdRef> + 'a, + moderator_id: impl types::IntoCow<'a, types::UserIdRef> + 'a, + ) -> Self { + Self { + broadcaster_id: broadcaster_id.into_cow(), + moderator_id: moderator_id.into_cow(), + } + } +} + +/// Body Parameters for [Update AutoMod Settings](super::update_automod_settings) +/// +/// [`update-automod-settings`](https://dev.twitch.tv/docs/api/reference#update-automod-settings) +/// +/// Because PUT is an overwrite operation, you must include all the fields that you want set after the operation completes. +/// Typically, you’ll send a GET request, update the fields you want to change, and pass that object in the PUT request. +/// +/// You may set either `overall_level` (`Overall`) or the individual settings like `aggression` (`Individual`), but not both. +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug)] +#[serde(untagged)] +#[non_exhaustive] +pub enum UpdateAutoModSettingsBody { + /// Set the `overall_level` + /// + /// Setting `overall_level` applies default values to the individual settings. + /// However, setting `overall_level` to 4 does not necessarily mean that it applies 4 to all the individual settings. + /// Instead, it applies a set of recommended defaults to the rest of the settings. + #[non_exhaustive] + Overall { + /// The default AutoMod level for the broadcaster. + overall_level: u8, + }, + /// Set the individual levels for each setting + Individual(UpdateAutoModSettingsIndividual), +} + +/// Set the individual levels for each setting +/// +/// Note that because PUT is an overwrite operation, you must include all the fields that you want set after the operation completes. +/// Use [from_settings](Self::from_settings) to initialize this struct to previously returned [AutoModSettings]. +#[derive(PartialEq, Eq, Deserialize, Serialize, Clone, Debug, Default)] +#[cfg_attr(feature = "typed-builder", derive(typed_builder::TypedBuilder))] +#[non_exhaustive] +pub struct UpdateAutoModSettingsIndividual { + /// The Automod level for hostility involving aggression. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub aggression: Option, + /// The Automod level for hostility involving name calling or insults. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub bullying: Option, + /// The Automod level for discrimination against disability. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub disability: Option, + /// The Automod level for discrimination against women. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub misogyny: Option, + /// The Automod level for racial discrimination. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub race_ethnicity_or_religion: Option, + /// The Automod level for sexual content. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub sex_based_terms: Option, + /// The AutoMod level for discrimination based on sexuality, sex, or gender. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub sexuality_sex_or_gender: Option, + /// The Automod level for profanity. + #[cfg_attr(feature = "typed-builder", builder(default))] + #[serde(skip_serializing_if = "Option::is_none")] + pub swearing: Option, +} + +impl helix::private::SealedSerialize for UpdateAutoModSettingsBody {} + +impl UpdateAutoModSettingsBody { + /// Set the `overall_level` + pub fn overall(overall_level: u8) -> Self { Self::Overall { overall_level } } + + /// Constructs an `Individual` from [AutoModSettings] + pub fn from_settings(settings: &AutoModSettings) -> Self { + Self::Individual(UpdateAutoModSettingsIndividual::from_settings(settings)) + } +} + +impl UpdateAutoModSettingsIndividual { + /// Constructs an update on individual settings from [AutoModSettings] + pub fn from_settings(settings: &AutoModSettings) -> Self { + Self { + aggression: Some(settings.aggression), + bullying: Some(settings.bullying), + disability: Some(settings.disability), + misogyny: Some(settings.misogyny), + race_ethnicity_or_religion: Some(settings.race_ethnicity_or_religion), + sex_based_terms: Some(settings.sex_based_terms), + sexuality_sex_or_gender: Some(settings.sexuality_sex_or_gender), + swearing: Some(settings.swearing), + } + } +} + +impl Request for UpdateAutoModSettingsRequest<'_> { + type Response = super::AutoModSettings; + + const PATH: &'static str = "moderation/automod/settings"; + #[cfg(feature = "twitch_oauth2")] + const SCOPE: twitch_oauth2::Validator = + twitch_oauth2::validator![twitch_oauth2::Scope::ModeratorManageAutomodSettings]; +} + +impl RequestPut for UpdateAutoModSettingsRequest<'_> { + type Body = UpdateAutoModSettingsBody; + + fn parse_inner_response( + request: Option, + uri: &http::Uri, + response: &str, + status: http::StatusCode, + ) -> Result::Response>, helix::HelixRequestPutError> + where + Self: Sized, + { + helix::parse_single_return(request, uri, response, status) + } +} + +#[cfg(test)] +#[test] +fn test_request_overall() { + use helix::*; + let req = UpdateAutoModSettingsRequest::new("1234", "5678"); + let body = UpdateAutoModSettingsBody::overall(3); + + assert_eq!( + std::str::from_utf8(&body.try_to_body().unwrap()).unwrap(), + r#"{"overall_level":3}"# + ); + + req.create_request(body, "token", "clientid").unwrap(); + + // From twitch docs + let data = br#" + { + "data": [ + { + "broadcaster_id": "1234", + "moderator_id": "5678", + "overall_level": 3, + "disability": 3, + "aggression": 3, + "sexuality_sex_or_gender": 3, + "misogyny": 3, + "bullying": 2, + "swearing": 0, + "race_ethnicity_or_religion": 3, + "sex_based_terms": 3 + } + ] + } + "# + .to_vec(); + + let http_response = http::Response::builder().status(200).body(data).unwrap(); + + let uri = req.get_uri().unwrap(); + assert_eq!( + uri.to_string(), + "https://api.twitch.tv/helix/moderation/automod/settings?broadcaster_id=1234&moderator_id=5678" + ); + + let res = UpdateAutoModSettingsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.overall_level, Some(3)); + assert_eq!(res.disability, 3); +} + +#[cfg(test)] +#[test] +fn test_request_individual() { + use helix::*; + let req = UpdateAutoModSettingsRequest::new("1234", "5678"); + let body = UpdateAutoModSettingsBody::Individual(UpdateAutoModSettingsIndividual { + aggression: Some(0), + bullying: Some(1), + disability: None, + misogyny: None, + race_ethnicity_or_religion: None, + sex_based_terms: None, + sexuality_sex_or_gender: None, + swearing: Some(2), + }); + + assert_eq!( + std::str::from_utf8(&body.try_to_body().unwrap()).unwrap(), + r#"{"aggression":0,"bullying":1,"swearing":2}"# + ); + + req.create_request(body, "token", "clientid").unwrap(); + + let data = br#" + { + "data": [ + { + "aggression": 0, + "broadcaster_id": "1234", + "bullying": 1, + "disability": 0, + "misogyny": 0, + "moderator_id": "5678", + "overall_level": null, + "race_ethnicity_or_religion": 0, + "sex_based_terms": 0, + "sexuality_sex_or_gender": 0, + "swearing": 2 + } + ] + } + "# + .to_vec(); + + let http_response = http::Response::builder().status(200).body(data).unwrap(); + + let uri = req.get_uri().unwrap(); + assert_eq!( + uri.to_string(), + "https://api.twitch.tv/helix/moderation/automod/settings?broadcaster_id=1234&moderator_id=5678" + ); + + let res = UpdateAutoModSettingsRequest::parse_response(Some(req), &uri, http_response) + .unwrap() + .data; + assert_eq!(res.overall_level, None); + assert_eq!(res.swearing, 2); +} diff --git a/src/helix/mod.rs b/src/helix/mod.rs index defe3eeb62..7dc87907f8 100644 --- a/src/helix/mod.rs +++ b/src/helix/mod.rs @@ -231,14 +231,14 @@ //! //!
//! -//!
Moderation 🟡 19/23 +//!
Moderation 🟡 20/23 //! //! | Endpoint | Helper | Module | //! |---|---|---| //! | [Check AutoMod Status](https://dev.twitch.tv/docs/api/reference#check-automod-status) | - | [`moderation::check_automod_status`] | //! | [Manage Held AutoMod Messages](https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages) | - | [`moderation::manage_held_automod_messages`] | //! | [Get AutoMod Settings](https://dev.twitch.tv/docs/api/reference#get-automod-settings) | - | [`moderation::get_automod_settings`] | -//! | [Update AutoMod Settings](https://dev.twitch.tv/docs/api/reference#update-automod-settings) | - | - | +//! | [Update AutoMod Settings](https://dev.twitch.tv/docs/api/reference#update-automod-settings) | - | [`moderation::update_automod_settings`] | //! | [Get Banned Users](https://dev.twitch.tv/docs/api/reference#get-banned-users) | - | [`moderation::get_banned_users`] | //! | [Ban User](https://dev.twitch.tv/docs/api/reference#ban-user) | [`HelixClient::ban_user`] | [`moderation::ban_user`] | //! | [Unban User](https://dev.twitch.tv/docs/api/reference#unban-user) | [`HelixClient::unban_user`] | [`moderation::unban_user`] | @@ -411,15 +411,14 @@ pub use request::errors::{ HelixRequestPostError, HelixRequestPutError, InvalidUri, SerializeError, }; #[doc(inline)] -pub use request::{ - parse_single_return, Request, RequestDelete, RequestGet, RequestPatch, RequestPost, RequestPut, -}; +pub use request::{Request, RequestDelete, RequestGet, RequestPatch, RequestPost, RequestPut}; #[doc(inline)] pub use response::Response; pub(crate) mod ser; pub(crate) use crate::deserialize_default_from_null; use crate::parse_json; +pub(crate) use request::parse_single_return; #[derive(PartialEq, Deserialize, Debug)] struct InnerResponse { diff --git a/src/helix/request.rs b/src/helix/request.rs index 2dfb662e3b..a9225aac4b 100644 --- a/src/helix/request.rs +++ b/src/helix/request.rs @@ -440,36 +440,30 @@ pub trait RequestGet: Request { /// Parses a response where Helix responds with a single datum inside `data`. /// /// An example response is `{ "data": [ { "foo": 1 } ]`. -pub fn parse_single_return( +pub(crate) fn parse_single_return( request: Option, uri: &http::Uri, response: &str, status: http::StatusCode, -) -> Result, HelixRequestGetError> +) -> Result, E> where - T: RequestGet, + T: Request, + E: errors::HelixRequestDeserError, { let resp = match status { http::StatusCode::OK => { - let resp: InnerResponse<[T::Response; 1]> = - parse_json(response, true).map_err(|e| { - HelixRequestGetError::DeserializeError( - response.to_string(), - e, - uri.clone(), - status, - ) - })?; + let resp: InnerResponse<[T::Response; 1]> = parse_json(response, true) + .map_err(|e| E::deserialize_error(response.to_string(), e, uri.clone(), status))?; let [s] = resp.data; s } _ => { - return Err(HelixRequestGetError::InvalidResponse { - reason: "unexpected status code", - response: response.to_string(), + return Err(E::invalid_response( + "unexpected status code", + response.to_string(), status, - uri: uri.clone(), - }) + uri.clone(), + )); } }; Ok(Response::with_data(resp, request)) diff --git a/src/helix/request/errors.rs b/src/helix/request/errors.rs index 422601137e..01b62e7366 100644 --- a/src/helix/request/errors.rs +++ b/src/helix/request/errors.rs @@ -251,3 +251,73 @@ pub enum HelixRequestDeleteError { uri: http::Uri, }, } + +/// Helper trait to allow construction of any error for an invalid response +pub(crate) trait HelixRequestError { + fn invalid_response( + reason: &'static str, + response: String, + status: http::StatusCode, + uri: http::Uri, + ) -> Self; +} + +/// Helper trait to allow construction of any error for a deserailization error (not available for DELETE requests) +pub(crate) trait HelixRequestDeserError: HelixRequestError { + fn deserialize_error( + body: String, + err: crate::DeserError, + uri: http::Uri, + status: http::StatusCode, + ) -> Self; +} + +macro_rules! impl_request_error { + ($($t:ty),*) => { + $(impl HelixRequestError for $t { + fn invalid_response( + reason: &'static str, + response: String, + status: http::StatusCode, + uri: http::Uri, + ) -> Self { + Self::InvalidResponse { + reason, + response, + status, + uri, + } + } + })* + }; +} + +macro_rules! impl_request_deser_error { + ($($t:ty),*) => { + $(impl HelixRequestDeserError for $t { + fn deserialize_error( + body: String, + err: crate::DeserError, + uri: http::Uri, + status: http::StatusCode, + ) -> Self { + Self::DeserializeError(body, err, uri, status) + } + })* + }; +} + +impl_request_error!( + HelixRequestGetError, + HelixRequestPatchError, + HelixRequestPostError, + HelixRequestPutError, + HelixRequestDeleteError +); + +impl_request_deser_error!( + HelixRequestGetError, + HelixRequestPatchError, + HelixRequestPostError, + HelixRequestPutError +);