From 29d213393b863aff6e013822299baff91539e643 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sat, 30 Dec 2023 13:42:52 -0500 Subject: [PATCH] feat(model, cache): Add support for application editing and new application fields (#2284) Ref: - https://github.com/discord/discord-api-docs/pull/6297 Additionally, this changes the path for `get_user_application` to be `/application/@me` instead of `/oauth2/applications/@me` for consistency. This returns the same data and shouldn't effect any users. --- twilight-http-ratelimiting/src/request.rs | 3 + twilight-http/src/client/mod.rs | 7 +- twilight-http/src/request/mod.rs | 2 + twilight-http/src/request/try_into_request.rs | 2 + .../src/request/update_user_application.rs | 180 ++++++++++++++++++ twilight-http/src/routing.rs | 23 ++- twilight-model/src/guild/mod.rs | 2 +- twilight-model/src/oauth/application.rs | 36 +++- .../current_authorization_information.rs | 11 +- 9 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 twilight-http/src/request/update_user_application.rs diff --git a/twilight-http-ratelimiting/src/request.rs b/twilight-http-ratelimiting/src/request.rs index f5351a9d18c..497f7bc97cc 100644 --- a/twilight-http-ratelimiting/src/request.rs +++ b/twilight-http-ratelimiting/src/request.rs @@ -123,6 +123,8 @@ pub enum Path { ApplicationGuildCommand(u64), /// Operating on a specific command in a guild. ApplicationGuildCommandId(u64), + /// Operating on current user application, + ApplicationsMe, /// Operating on a channel. ChannelsId(u64), /// Operating on a channel's followers. @@ -330,6 +332,7 @@ impl FromStr for Path { let parts = s.split('/').skip(skip).collect::>(); Ok(match parts[..] { + ["applications", "me"] => ApplicationsMe, ["applications", id, "commands"] => ApplicationCommand(parse_id(id)?), ["applications", id, "commands", _] => ApplicationCommandId(parse_id(id)?), ["applications", id, "guilds", _, "commands"] diff --git a/twilight-http/src/client/mod.rs b/twilight-http/src/client/mod.rs index 1f6e1ed8c4c..0ecb50de3b9 100644 --- a/twilight-http/src/client/mod.rs +++ b/twilight-http/src/client/mod.rs @@ -9,7 +9,7 @@ use crate::request::{ update_guild_onboarding::{UpdateGuildOnboarding, UpdateGuildOnboardingFields}, GetGuildOnboarding, }, - GetCurrentAuthorizationInformation, + GetCurrentAuthorizationInformation, UpdateCurrentUserApplication, }; #[allow(deprecated)] use crate::{ @@ -698,6 +698,11 @@ impl Client { GetUserApplicationInfo::new(self) } + /// Update the current user's application. + pub const fn update_current_user_application(&self) -> UpdateCurrentUserApplication<'_> { + UpdateCurrentUserApplication::new(self) + } + /// Update the current user. /// /// All parameters are optional. If the username is changed, it may cause the discriminator to diff --git a/twilight-http/src/request/mod.rs b/twilight-http/src/request/mod.rs index 1f694aeafc7..327ac390137 100644 --- a/twilight-http/src/request/mod.rs +++ b/twilight-http/src/request/mod.rs @@ -58,6 +58,7 @@ mod get_user_application; mod get_voice_regions; mod multipart; mod try_into_request; +mod update_user_application; pub use self::{ audit_reason::AuditLogReason, @@ -69,6 +70,7 @@ pub use self::{ get_voice_regions::GetVoiceRegions, multipart::Form, try_into_request::TryIntoRequest, + update_user_application::UpdateCurrentUserApplication, }; pub use twilight_http_ratelimiting::request::Method; diff --git a/twilight-http/src/request/try_into_request.rs b/twilight-http/src/request/try_into_request.rs index b950bd36f35..18af4055a4c 100644 --- a/twilight-http/src/request/try_into_request.rs +++ b/twilight-http/src/request/try_into_request.rs @@ -82,6 +82,7 @@ mod private { CreateGuildFromTemplate, CreateTemplate, DeleteTemplate, GetTemplate, GetTemplates, SyncTemplate, UpdateTemplate, }, + update_user_application::UpdateCurrentUserApplication, user::{ CreatePrivateChannel, GetCurrentUser, GetCurrentUserConnections, GetCurrentUserGuildMember, GetCurrentUserGuilds, GetUser, LeaveGuild, @@ -269,6 +270,7 @@ mod private { impl Sealed for UpdateWebhook<'_> {} impl Sealed for UpdateWebhookMessage<'_> {} impl Sealed for UpdateWebhookWithToken<'_> {} + impl Sealed for UpdateCurrentUserApplication<'_> {} } use super::base::Request; diff --git a/twilight-http/src/request/update_user_application.rs b/twilight-http/src/request/update_user_application.rs new file mode 100644 index 00000000000..fbd251b0551 --- /dev/null +++ b/twilight-http/src/request/update_user_application.rs @@ -0,0 +1,180 @@ +use std::future::IntoFuture; + +use serde::Serialize; +use twilight_model::oauth::{Application, ApplicationFlags, InstallParams}; + +use crate::{ + client::Client, + error::Error, + request::{Nullable, Request, TryIntoRequest}, + response::{Response, ResponseFuture}, + routing::Route, +}; + +#[derive(Serialize)] +struct UpdateCurrentUserApplicationFields<'a> { + #[serde(skip_serializing_if = "Option::is_none")] + cover_image: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + custom_install_url: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + flags: Option, + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + install_params: Option, + #[serde(skip_serializing_if = "Option::is_none")] + interactions_endpoint_url: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + role_connections_verification_url: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option>, +} + +/// Update the current user's application. +/// +/// Returns the newly updated application. +/// +/// Refer to [Discord Docs/Update Current User Application][1]. +/// +/// # Examples +/// +/// ```no_run +/// # #[tokio::main] async fn main() -> Result<(), Box> { +/// use std::env; +/// use twilight_http::Client; +/// +/// let bearer_token = env::var("BEARER_TOKEN")?; +/// +/// let client = Client::new(bearer_token); +/// let response = client +/// .update_current_user_application() +/// .description("My cool application") +/// .await?; +/// let application = response.model().await?; +/// +/// println!("Application: {}", application.description); +/// +/// # Ok(()) } +/// ``` +/// +/// [1]: https://discord.com/developers/docs/resources/application#edit-current-application +#[must_use = "requests must be configured and executed"] +pub struct UpdateCurrentUserApplication<'a> { + fields: UpdateCurrentUserApplicationFields<'a>, + http: &'a Client, +} + +impl<'a> UpdateCurrentUserApplication<'a> { + pub(crate) const fn new(http: &'a Client) -> Self { + Self { + fields: UpdateCurrentUserApplicationFields { + cover_image: None, + custom_install_url: None, + description: None, + flags: None, + icon: None, + install_params: None, + interactions_endpoint_url: None, + role_connections_verification_url: None, + tags: None, + }, + http, + } + } + + /// Sets the cover image of the application. + pub const fn cover_image(mut self, cover_image: Option<&'a str>) -> Self { + self.fields.cover_image = Some(Nullable(cover_image)); + + self + } + + /// Sets the custom install URL of the application. + pub const fn custom_install_url(mut self, custom_install_url: &'a str) -> Self { + self.fields.custom_install_url = Some(custom_install_url); + + self + } + + /// Sets the description of the application. + pub const fn description(mut self, description: &'a str) -> Self { + self.fields.description = Some(description); + + self + } + + /// Sets the flags of the application. + /// Only limited intent flags (`GATEWAY_PRESENCE_LIMITED`, `GATEWAY_GUILD_MEMBERS_LIMITED`, + /// and `GATEWAY_MESSAGE_CONTENT_LIMITED`) can be updated via the API. + pub const fn flags(mut self, flags: ApplicationFlags) -> Self { + self.fields.flags = Some(flags); + + self + } + + /// Sets the icon of the application. + pub const fn icon(mut self, icon: Option<&'a str>) -> Self { + self.fields.icon = Some(Nullable(icon)); + + self + } + + /// Sets the install params of the application. + pub fn install_params(mut self, install_params: InstallParams) -> Self { + self.fields.install_params = Some(install_params); + + self + } + + /// Sets the interactions endpoint URL of the application. + pub const fn interactions_endpoint_url(mut self, interactions_endpoint_url: &'a str) -> Self { + self.fields.interactions_endpoint_url = Some(interactions_endpoint_url); + + self + } + + /// Sets the role connections verification URL of the application. + pub const fn role_connections_verification_url( + mut self, + role_connections_verification_url: &'a str, + ) -> Self { + self.fields.role_connections_verification_url = Some(role_connections_verification_url); + + self + } + + /// Sets the tags of the application. + pub fn tags(mut self, tags: Vec<&'a str>) -> Self { + self.fields.tags = Some(tags); + + self + } +} + +impl IntoFuture for UpdateCurrentUserApplication<'_> { + type Output = Result, Error>; + + type IntoFuture = ResponseFuture; + + fn into_future(self) -> Self::IntoFuture { + let http = self.http; + + match self.try_into_request() { + Ok(request) => http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} + +impl TryIntoRequest for UpdateCurrentUserApplication<'_> { + fn try_into_request(self) -> Result { + let mut request = Request::builder(&Route::UpdateCurrentUserApplication); + + request = request.json(&self.fields)?; + + Ok(request.build()) + } +} diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index 6c15da4ecec..18afe16bdbe 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -9,7 +9,10 @@ use twilight_model::id::{marker::RoleMarker, Id}; #[non_exhaustive] pub enum Route<'a> { /// Route information to add a user to a guild. - AddGuildMember { guild_id: u64, user_id: u64 }, + AddGuildMember { + guild_id: u64, + user_id: u64, + }, /// Route information to add a role to guild member. AddMemberRole { /// The ID of the guild. @@ -1099,6 +1102,7 @@ pub enum Route<'a> { token: &'a str, webhook_id: u64, }, + UpdateCurrentUserApplication, } impl<'a> Route<'a> { @@ -1249,6 +1253,7 @@ impl<'a> Route<'a> { | Self::UpdateTemplate { .. } | Self::UpdateUserVoiceState { .. } | Self::UpdateWebhookMessage { .. } + | Self::UpdateCurrentUserApplication | Self::UpdateWebhook { .. } => Method::Patch, Self::CreateChannel { .. } | Self::CreateGlobalCommand { .. } @@ -1537,7 +1542,9 @@ impl<'a> Route<'a> { Path::ApplicationGuildCommandId(application_id) } Self::GetCurrentAuthorizationInformation => Path::OauthMe, - Self::GetCurrentUserApplicationInfo => Path::OauthApplicationsMe, + Self::GetCurrentUserApplicationInfo | Self::UpdateCurrentUserApplication => { + Path::ApplicationsMe + } Self::GetCurrentUser | Self::GetUser { .. } | Self::UpdateCurrentUser => Path::UsersId, Self::GetCurrentUserGuildMember { .. } => Path::UsersIdGuildsIdMember, Self::GetEmoji { guild_id, .. } | Self::UpdateEmoji { guild_id, .. } => { @@ -2341,7 +2348,9 @@ impl Display for Route<'_> { f.write_str("/permissions") } Route::GetCurrentAuthorizationInformation => f.write_str("oauth2/@me"), - Route::GetCurrentUserApplicationInfo => f.write_str("oauth2/applications/@me"), + Route::GetCurrentUserApplicationInfo | Route::UpdateCurrentUserApplication => { + f.write_str("applications/@me") + } Route::GetCurrentUser | Route::UpdateCurrentUser => f.write_str("users/@me"), Route::GetCurrentUserGuildMember { guild_id } => { f.write_str("users/@me/guilds/")?; @@ -4005,7 +4014,13 @@ mod tests { #[test] fn get_current_user_application_info() { let route = Route::GetCurrentUserApplicationInfo; - assert_eq!(route.to_string(), "oauth2/applications/@me"); + assert_eq!(route.to_string(), "applications/@me"); + } + + #[test] + fn update_current_user_application() { + let route = Route::UpdateCurrentUserApplication; + assert_eq!(route.to_string(), "applications/@me"); } #[test] diff --git a/twilight-model/src/guild/mod.rs b/twilight-model/src/guild/mod.rs index 462e4ae5c71..3a26bc830cf 100644 --- a/twilight-model/src/guild/mod.rs +++ b/twilight-model/src/guild/mod.rs @@ -76,7 +76,7 @@ use serde::{ }; use std::fmt::{Formatter, Result as FmtResult}; -#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Hash)] pub struct Guild { pub afk_channel_id: Option>, pub afk_timeout: AfkTimeout, diff --git a/twilight-model/src/oauth/application.rs b/twilight-model/src/oauth/application.rs index ad6e64a9a36..1d1067af35c 100644 --- a/twilight-model/src/oauth/application.rs +++ b/twilight-model/src/oauth/application.rs @@ -1,5 +1,6 @@ use super::{team::Team, ApplicationFlags, InstallParams}; use crate::{ + guild::Guild, id::{ marker::{ApplicationMarker, GuildMarker, OauthSkuMarker}, Id, @@ -11,6 +12,12 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Application { + /// Approximate count of guilds this app has been added to. + #[serde(skip_serializing_if = "Option::is_none")] + pub approximate_guild_count: Option, + /// Partial user object for the bot user associated with the app. + #[serde(skip_serializing_if = "Option::is_none")] + pub bot: Option, pub bot_public: bool, pub bot_require_code_grant: bool, /// Default rich presence invite cover image. @@ -21,6 +28,9 @@ pub struct Application { /// Description of the application. pub description: String, pub guild_id: Option>, + /// Partial object of the associated guild. + #[serde(skip_serializing_if = "Option::is_none")] + pub guild: Option, /// Public flags of the application. pub flags: Option, /// Icon of the application. @@ -30,6 +40,9 @@ pub struct Application { /// Settings for the application's default in-app authorization, if enabled. #[serde(skip_serializing_if = "Option::is_none")] pub install_params: Option, + /// Interactions endpoint URL for the app. + #[serde(skip_serializing_if = "Option::is_none")] + pub interactions_endpoint_url: Option, /// Name of the application. pub name: String, pub owner: Option, @@ -37,8 +50,14 @@ pub struct Application { /// URL of the application's privacy policy. #[serde(skip_serializing_if = "Option::is_none")] pub privacy_policy_url: Option, + /// Role connection verification URL for the app. + #[serde(skip_serializing_if = "Option::is_none")] + pub role_connections_verification_url: Option, #[serde(default)] pub rpc_origins: Vec, + /// Redirect URIs for the application. + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect_uris: Option>, pub slug: Option, /// Tags describing the content and functionality of the application. #[serde(skip_serializing_if = "Option::is_none")] @@ -96,16 +115,20 @@ mod tests { #[test] fn current_application_info() { let value = Application { + approximate_guild_count: Some(2), + bot: None, bot_public: true, bot_require_code_grant: false, cover_image: Some(image_hash::COVER), custom_install_url: None, description: "a pretty cool application".to_owned(), guild_id: Some(Id::new(1)), + guild: None, flags: Some(ApplicationFlags::EMBEDDED), icon: Some(image_hash::ICON), id: Id::new(2), install_params: None, + interactions_endpoint_url: Some("https://interactions".into()), name: "cool application".to_owned(), owner: Some(User { accent_color: None, @@ -128,6 +151,8 @@ mod tests { }), primary_sku_id: Some(Id::new(4)), privacy_policy_url: Some("https://privacypolicy".into()), + redirect_uris: None, + role_connections_verification_url: Some("https://roleconnections".into()), rpc_origins: vec!["one".to_owned()], slug: Some("app slug".to_owned()), tags: Some(Vec::from([ @@ -152,8 +177,11 @@ mod tests { &[ Token::Struct { name: "Application", - len: 18, + len: 21, }, + Token::Str("approximate_guild_count"), + Token::Some, + Token::U64(2), Token::Str("bot_public"), Token::Bool(true), Token::Str("bot_require_code_grant"), @@ -176,6 +204,9 @@ mod tests { Token::Str("id"), Token::NewtypeStruct { name: "Id" }, Token::Str("2"), + Token::Str("interactions_endpoint_url"), + Token::Some, + Token::Str("https://interactions"), Token::Str("name"), Token::Str("cool application"), Token::Str("owner"), @@ -212,6 +243,9 @@ mod tests { Token::Str("privacy_policy_url"), Token::Some, Token::Str("https://privacypolicy"), + Token::Str("role_connections_verification_url"), + Token::Some, + Token::Str("https://roleconnections"), Token::Str("rpc_origins"), Token::Seq { len: Some(1) }, Token::Str("one"), diff --git a/twilight-model/src/oauth/current_authorization_information.rs b/twilight-model/src/oauth/current_authorization_information.rs index b9e25aef5a3..dec0901c7cb 100644 --- a/twilight-model/src/oauth/current_authorization_information.rs +++ b/twilight-model/src/oauth/current_authorization_information.rs @@ -71,20 +71,26 @@ mod tests { let value = CurrentAuthorizationInformation { application: Application { + approximate_guild_count: Some(2), + bot: None, bot_public: true, bot_require_code_grant: true, cover_image: None, custom_install_url: None, description: DESCRIPTION.to_owned(), guild_id: None, + guild: None, flags: None, icon: Some(image_hash::ICON), id: Id::new(100_000_000_000_000_000), install_params: None, + interactions_endpoint_url: None, name: NAME.to_owned(), owner: None, primary_sku_id: None, privacy_policy_url: None, + redirect_uris: None, + role_connections_verification_url: None, rpc_origins: Vec::new(), slug: None, tags: None, @@ -107,8 +113,11 @@ mod tests { Token::Str("application"), Token::Struct { name: "Application", - len: 15, + len: 16, }, + Token::Str("approximate_guild_count"), + Token::Some, + Token::U64(2), Token::Str("bot_public"), Token::Bool(true), Token::Str("bot_require_code_grant"),