From b87df594845f34d92cf30c6fc111cefe0b92de9d Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:38:33 +0200 Subject: [PATCH 01/23] Implement custom sqlx::Postgres en-/decoding for PremiumType enum (#565) --- src/types/entities/user.rs | 48 +++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index bd74a8cb..5e761307 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -233,9 +233,7 @@ bitflags::bitflags! { PartialOrd, Ord, )] -#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] -#[cfg_attr(not(feature = "sqlx"), repr(u8))] -#[cfg_attr(feature = "sqlx", repr(i16))] +#[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] /// **User** premium (Nitro) type /// @@ -252,6 +250,50 @@ pub enum PremiumType { Tier3 = 3, } +impl TryFrom for PremiumType { + type Error = ChorusError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Tier1), + 2 => Ok(Self::Tier2), + 3 => Ok(Self::Tier3), + _ => Err(ChorusError::InvalidArguments { + error: "Value is not a valid PremiumType".to_string(), + }), + } + } +} + +#[cfg(feature = "sqlx")] +impl sqlx::Type for PremiumType { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for PremiumType { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::from(*self as u8); + sqlx_pg_uint.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for PremiumType { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::decode(value)?; + PremiumType::try_from(sqlx_pg_uint.to_uint()).map_err(|e| e.into()) + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] /// # Reference /// See From 515750c2a6f9346e699d74f35073c479ab2b90d9 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:37:35 +0200 Subject: [PATCH 02/23] Make Relationship struct sqlx/symfonia-ready (#567) --- src/types/entities/relationship.rs | 60 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index 08cb41f1..e6d14f42 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -6,18 +6,28 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::errors::ChorusError; use crate::types::{Shared, Snowflake}; use super::{arc_rwlock_ptr_eq, PublicUser}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] /// See pub struct Relationship { + /// The ID of the target user + #[cfg_attr(feature = "sqlx", sqlx(rename = "to_id"))] pub id: Snowflake, #[serde(rename = "type")] + #[cfg_attr(feature = "sqlx", sqlx(rename = "type"))] pub relationship_type: RelationshipType, + #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id + /// The nickname of the user in this relationship pub nickname: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id + /// The target user pub user: Shared, + /// When the user requested a relationship pub since: Option>, } @@ -45,8 +55,7 @@ impl PartialEq for Relationship { Copy, Hash, )] -#[cfg_attr(not(feature = "sqlx"), repr(u8))] -#[cfg_attr(feature = "sqlx", repr(i16))] +#[repr(u8)] /// See pub enum RelationshipType { Suggestion = 6, @@ -58,3 +67,50 @@ pub enum RelationshipType { Friends = 1, None = 0, } + +#[cfg(feature = "sqlx")] +impl sqlx::Type for RelationshipType { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for RelationshipType { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::from(*self as u8); + sqlx_pg_uint.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for RelationshipType { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::decode(value)?; + Self::try_from(sqlx_pg_uint.to_uint()).map_err(|e| e.into()) + } +} + +impl TryFrom for RelationshipType { + type Error = ChorusError; + + fn try_from(value: u8) -> Result { + match value { + 6 => Ok(Self::Suggestion), + 5 => Ok(Self::Implicit), + 4 => Ok(Self::Outgoing), + 3 => Ok(Self::Incoming), + 2 => Ok(Self::Blocked), + 1 => Ok(Self::Friends), + 0 => Ok(Self::None), + _ => Err(ChorusError::InvalidArguments { + error: format!("Value {} is not a valid RelationshipType", value), + }), + } + } +} From c52012e55b3f12960b570414c2719c94bc75090e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 8 Oct 2024 17:18:39 +0200 Subject: [PATCH 03/23] #567: Append: Nickname *cannot* be derived from user id --- src/types/entities/relationship.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index e6d14f42..da2a9dc7 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -21,7 +21,6 @@ pub struct Relationship { #[serde(rename = "type")] #[cfg_attr(feature = "sqlx", sqlx(rename = "type"))] pub relationship_type: RelationshipType, - #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id /// The nickname of the user in this relationship pub nickname: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id From 7b50d56783c306b3eab0887b380bc0120beab7aa Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 28 Apr 2024 14:15:57 +0200 Subject: [PATCH 04/23] Move Shared to types/mod.rs, bump some dependencies (#492) * deps: bump rustls to 0.21.11 This is done to fix CVE-2024-32650, which practically shouldn't affect us but it's still better not to use vulnerable dependencies. * deps: bump h2 to 0.3.26 This is done to fix another vulnerability, which should also not affect us (non-critical, in h2 servers) * fix: move Shared to types/mod.rs --- src/types/entities/integration.rs | 1 + src/types/entities/message.rs | 1 + src/types/entities/template.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index 50a82819..b119e55c 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::{ + Shared, entities::{Application, User}, utils::Snowflake, Shared, diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index b63eb3bf..3fd7b43f 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::{ + Shared, entities::{ Application, Attachment, Channel, Emoji, GuildMember, PublicUser, RoleSubscriptionData, Sticker, StickerItem, User, diff --git a/src/types/entities/template.rs b/src/types/entities/template.rs index 29e53689..f95bb574 100644 --- a/src/types/entities/template.rs +++ b/src/types/entities/template.rs @@ -6,6 +6,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::{ + Shared, entities::{Guild, User}, utils::Snowflake, Shared, From 673181e8f316516f79396949cb8d7743c98ba841 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 5 May 2024 14:43:23 +0200 Subject: [PATCH 05/23] Refactor / fix login and register (#495) Change login and register to only use one ChorusUser object, change the api of related methods which were also somewhat ugly Feature lock different types for UserSettings::status Add sqlx::FromRow derive to GuildMember Use Snowflake in Claims Use ChannelType enum on ChannelModifySchema Add From> impl for GuildFeaturesList Add feature sqlx locks for user, roles on GuildMember Use distinct type for explicit_content_filter Revert c4452132 Fix errors in documentation tests update dev-dependencies Expand documentation to explain facade type Fix oversight for premium_since Add sqlx Type, Encode, Decode impl for InviteFlags bitflag object. Fix inverted type wrapping Remove unused imports, feature locks in macro forgot a file :( Fix test I feel silly. Fix compilation for real, no dirty hack u8 -> u64 --- Cargo.toml | 6 ++++++ src/api/auth/login.rs | 10 ++++++++++ src/api/auth/mod.rs | 6 ++++++ src/api/auth/register.rs | 10 ++++++++++ src/api/users/users.rs | 8 ++++++++ src/types/schema/channel.rs | 23 +++++++++++++++++++++++ src/types/utils/serde.rs | 12 ++++++------ 7 files changed, 69 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d4830ed..42e23836 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ wasm-bindgen-futures = "0.4.43" wasmtimer = "0.2.0" [dev-dependencies] +<<<<<<< HEAD lazy_static = "1.5.0" wasm-bindgen-test = "0.3.43" wasm-bindgen = "0.2.93" @@ -91,3 +92,8 @@ simple_logger = { version = "5.0.0", default-features = false } [lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } +======= +lazy_static = "1.4.0" +wasm-bindgen-test = "0.3.42" +wasm-bindgen = "0.2.92" +>>>>>>> 50c5c29 (update dev-dependencies) diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 3a9a9eeb..1af19e05 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -30,12 +30,22 @@ impl Instance { // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since login is an instance wide limit), which is why we are just cloning the // instances' limits to pass them on as user_rate_limits later. +<<<<<<< HEAD let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None").await; let login_result = chorus_request .deserialize_response::(&mut user) .await?; user.set_token(&login_result.token); +======= + let mut user = + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; + + let login_result = chorus_request + .deserialize_response::(&mut user) + .await?; + user.set_token(login_result.token); +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) user.settings = login_result.settings; let object = User::get(&mut user, None).await?; diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index b9050e14..7aaf13f5 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -22,8 +22,14 @@ pub mod register; impl Instance { /// Logs into an existing account on the spacebar server, using only a token. +<<<<<<< HEAD pub async fn login_with_token(&mut self, token: &str) -> ChorusResult { let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; +======= + pub async fn login_with_token(&mut self, token: String) -> ChorusResult { + let mut user = + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) let object = User::get(&mut user, None).await?; let settings = User::get_settings(&mut user).await?; diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 821a52ff..532a65c6 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -37,13 +37,23 @@ impl Instance { // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since register is an instance wide limit), which is why we are just cloning // the instances' limits to pass them on as user_rate_limits later. +<<<<<<< HEAD let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None").await; +======= + let mut user = + ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; + +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) let token = chorus_request .deserialize_response::(&mut user) .await? .token; +<<<<<<< HEAD user.set_token(&token); +======= + user.set_token(token); +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) let object = User::get(&mut user, None).await?; let settings = User::get_settings(&mut user).await?; diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 4f6ef579..3f43efbf 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -32,7 +32,11 @@ impl ChorusUser { /// # Notes /// This function is a wrapper around [`User::get_settings`]. pub async fn get_settings(&mut self) -> ChorusResult { +<<<<<<< HEAD User::get_settings(self).await +======= + User::get_settings(self).await +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) } /// Modifies the current user's representation. (See [`User`]) @@ -138,3 +142,7 @@ impl User { } } } +<<<<<<< HEAD +======= + +>>>>>>> 03f1e7d (Refactor / fix login and register (#495)) diff --git a/src/types/schema/channel.rs b/src/types/schema/channel.rs index 62b3b53a..fea2545a 100644 --- a/src/types/schema/channel.rs +++ b/src/types/schema/channel.rs @@ -138,6 +138,29 @@ bitflags! { } } +#[cfg(feature = "sqlx")] +impl sqlx::Type for InviteFlags { + fn type_info() -> sqlx::mysql::MySqlTypeInfo { + u64::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::MySql> for InviteFlags { + fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> sqlx::encode::IsNull { + u64::encode_by_ref(&self.0.0, buf) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::MySql> for InviteFlags { + fn decode(value: >::ValueRef) -> Result { + let raw = u64::decode(value)?; + + Ok(Self::from_bits(raw).unwrap()) + } +} + #[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] diff --git a/src/types/utils/serde.rs b/src/types/utils/serde.rs index da41f4ac..f682bb0b 100644 --- a/src/types/utils/serde.rs +++ b/src/types/utils/serde.rs @@ -32,7 +32,7 @@ pub struct SecondsStringTimestampVisitor; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); /// let my_s: S = serde_json::from_str(&as_string)?; /// assert_eq!(my_s.time, time); -/// # Ok::<(), serde_json::Error>(()) +/// // Ok::<(), serde_json::Error>(()) /// ``` pub mod ts_seconds_str { @@ -64,7 +64,7 @@ pub mod ts_seconds_str { /// }; /// let as_string = serde_json::to_string(&my_s)?; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); - /// # Ok::<(), serde_json::Error>(()) + /// // Ok::<(), serde_json::Error>(()) /// ``` pub fn serialize(dt: &DateTime, serializer: S) -> Result where @@ -91,7 +91,7 @@ pub mod ts_seconds_str { /// /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).unwrap() }); - /// # Ok::<(), serde_json::Error>(()) + /// // Ok::<(), serde_json::Error>(()) /// ``` pub fn deserialize<'de, D>(d: D) -> Result, D::Error> where @@ -145,7 +145,7 @@ pub mod ts_seconds_str { /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); /// let my_s: S = serde_json::from_str(&as_string)?; /// assert_eq!(my_s.time, time); -/// # Ok::<(), serde_json::Error>(()) +/// // Ok::<(), serde_json::Error>(()) /// ``` pub mod ts_seconds_option_str { use super::SecondsStringTimestampVisitor; @@ -174,7 +174,7 @@ pub mod ts_seconds_option_str { /// }; /// let as_string = serde_json::to_string(&my_s)?; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); - /// # Ok::<(), serde_json::Error>(()) + /// // Ok::<(), serde_json::Error>(()) /// ``` pub fn serialize(opt: &Option>, serializer: S) -> Result where @@ -204,7 +204,7 @@ pub mod ts_seconds_option_str { /// /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).single() }); - /// # Ok::<(), serde_json::Error>(()) + /// // Ok::<(), serde_json::Error>(()) /// ``` pub fn deserialize<'de, D>(d: D) -> Result>, D::Error> where From f5c4fc39249d09ffbb203fe3caa700d3ba32e7f4 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:23:13 +0200 Subject: [PATCH 06/23] Implement gateway options, zlib-stream compression (#508) * feat: add GatewayOptions * feat: implement zlib-stream compression This also changes how gateway messages work. Now each gateway backend converts its message into an intermediary RawGatewayMessage, from which we inflate and parse GatewayMessages. Thanks to ByteAlex and their zlib-stream-rs crate, which helped me understand how to parse a compressed websocket stream --- Cargo.lock | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2d79f4a4..f36d5a4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,6 +598,16 @@ dependencies = [ "miniz_oxide 0.8.0", ] +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.0" From 17e46db3bec6d455670c329b5c2ac5a16417aa9e Mon Sep 17 00:00:00 2001 From: Quat3rnion <81202811+Quat3rnion@users.noreply.github.com> Date: Thu, 27 Jun 2024 02:45:51 -0400 Subject: [PATCH 07/23] Backend/guilds (#509) * Fix SQL encode/decode for GuildFeatures * Use distinct PermissionFlags type * Add Emoji schema types, modify GuildBan with feature lock * Add Schemas for pruning guild members * Add schemas for interfacing with stickers backend routes * Add schemas for interfacing with vanity-url backend routes * Add schema for interfacing with guilds/id/welcome-screen route * Make all Option types Vec types with #[serde(default)] * Add various types to support guilds/* api routes * Add missing enums and structs for searching messages * Use proper distinct types * Add EmbedType enum * Use distinct PermissionFlags type * Changes supporting backend for VoiceState * Changes supporting backend for AuditLog's Fix voice, voice_udp features Add one BILLION derives Fix: Wrong function name Fix: Turn unconditional import of sqlx::types::Json into conditional one Fix: Compile error with no default features Update CONTRIBUTING.md Fix/Correct UnavailableGuild object Fix testcase that relied on false behavior implemented by older spacebar servers Increase limit integer size to match spacebars' possibilities forgor installing nextest Bump browser_version according to https://www.useragents.me/#most-common-desktop-useragents Fix voice_simple example Update documentation in gateway_observers example Readd Observer trait as reexport remove cargo lock from example More accurate "GatewayHello::default()" Manually implement std::default::Default for GatewayHeartbeat and GatewayHeartbeatAck bump versions of packages to latest compatible versions --- .github/workflows/build_and_test.yml | 2 ++ Cargo.lock | 14 ++++++++++++++ Cargo.toml | 27 +++++++++++++++++++++++++++ src/types/entities/integration.rs | 1 - src/types/entities/message.rs | 3 ++- src/types/schema/role.rs | 1 + 6 files changed, 46 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 547e2e83..b9cebea5 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -97,6 +97,7 @@ jobs: with: cache-all-crates: "true" prefix-key: "macos" + - uses: taiki-e/install-action@nextest - name: Run WASM tests with Safari, Firefox, Chrome run: | rustup target add wasm32-unknown-unknown @@ -126,6 +127,7 @@ jobs: with: cache-all-crates: "true" prefix-key: "macos" + - uses: taiki-e/install-action@nextest - name: Run WASM tests with Safari, Firefox, Chrome run: | rustup target add wasm32-unknown-unknown diff --git a/Cargo.lock b/Cargo.lock index f36d5a4f..f9e2ec43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1280,6 +1280,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "miniz_oxide" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1294,6 +1295,13 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ +======= +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) "hermit-abi", "libc", "wasi", @@ -2623,9 +2631,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" +<<<<<<< HEAD version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +======= +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 42e23836..6c35eb39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,15 @@ voice_gateway = [] sqlx-pg-uint = ["dep:sqlx-pg-uint", "sqlx-pg-uint/serde"] [dependencies] +<<<<<<< HEAD tokio = { version = "1.39.3", features = ["macros", "sync"] } serde = { version = "1.0.209", features = ["derive", "rc"] } serde_json = { version = "1.0.127", features = ["raw_value"] } +======= +tokio = { version = "1.38.1", features = ["macros", "sync"] } +serde = { version = "1.0.204", features = ["derive", "rc"] } +serde_json = { version = "1.0.120", features = ["raw_value"] } +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) serde-aux = "4.5.0" serde_with = "3.9.0" serde_repr = "0.1.19" @@ -36,7 +42,11 @@ reqwest = { features = [ ], version = "=0.11.26", default-features = false } url = "2.5.2" chrono = { version = "0.4.38", features = ["serde"] } +<<<<<<< HEAD regex = "1.10.6" +======= +regex = "1.10.5" +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) custom_error = "1.9.2" futures-util = "0.3.30" http = "0.2.12" @@ -49,7 +59,13 @@ jsonwebtoken = "8.3.0" log = "0.4.22" async-trait = "0.1.81" chorus-macros = { path = "./chorus-macros", version = "0" } # Note: version here is used when releasing. This will use the latest release. Make sure to republish the crate when code in macros is changed! +<<<<<<< HEAD sqlx = { version = "0.8.1", features = [ +======= +sqlx = { version = "0.7.4", features = [ + "mysql", + "sqlite", +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) "json", "chrono", "ipnetwork", @@ -67,7 +83,10 @@ rand = "0.8.5" flate2 = { version = "1.0.33", optional = true } webpki-roots = "0.26.3" pubserve = { version = "1.1.0", features = ["async", "send"] } +<<<<<<< HEAD sqlx-pg-uint = { version = "0.5.0", features = ["serde"], optional = true } +======= +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = "0.21.12" @@ -80,6 +99,7 @@ getrandom = { version = "0.2.15" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.15", features = ["js"] } ws_stream_wasm = "0.7.4" +<<<<<<< HEAD wasm-bindgen-futures = "0.4.43" wasmtimer = "0.2.0" @@ -94,6 +114,13 @@ simple_logger = { version = "5.0.0", default-features = false } unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } ======= lazy_static = "1.4.0" +======= +wasm-bindgen-futures = "0.4.42" +wasmtimer = "0.2.0" + +[dev-dependencies] +lazy_static = "1.5.0" +>>>>>>> f2f45b4 (bump versions of packages to latest compatible versions) wasm-bindgen-test = "0.3.42" wasm-bindgen = "0.2.92" >>>>>>> 50c5c29 (update dev-dependencies) diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index b119e55c..50a82819 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -6,7 +6,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::{ - Shared, entities::{Application, User}, utils::Snowflake, Shared, diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index 3fd7b43f..ef7f392e 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::{ - Shared, entities::{ Application, Attachment, Channel, Emoji, GuildMember, PublicUser, RoleSubscriptionData, Sticker, StickerItem, User, @@ -20,6 +19,8 @@ use crate::{UInt32, UInt8}; use super::option_arc_rwlock_ptr_eq; +use super::option_arc_rwlock_ptr_eq; + #[derive(Debug, Serialize, Deserialize, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] /// Represents a message sent in a channel. diff --git a/src/types/schema/role.rs b/src/types/schema/role.rs index 805d0263..17292fea 100644 --- a/src/types/schema/role.rs +++ b/src/types/schema/role.rs @@ -4,6 +4,7 @@ use crate::types::{PermissionFlags, Snowflake}; use serde::{Deserialize, Serialize}; +use crate::types::{PermissionFlags, Snowflake}; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] From b6ca485b8226604613d34bfad810492d84a12ae0 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Wed, 21 Aug 2024 23:43:42 +0200 Subject: [PATCH 08/23] Initial support for PostgreSQL (#548) * Change sqlx::Any to sqlx::Postgres * Change sqlx::Any to sqlx::Postgres * Remove JSONified overrides when sqlx feature is enabled, where it makes sense * Add num-bigint dep * Remove generic impl for From for Snowflake For some reason, this trait bound conflicts with another trait bound from the sqlx-pg-uint crate, even though I personally don't get why. * Remove num_bigint, adsd sqlx-pg-uint * swap u64 for PgU64 in some files * use v0.3.0 of sqlx-pg-uint * Lots of sqlx-postgres type changes * Lots of sqlx-postgres type changes * gwah * Change repr(i8) to repr(i16) in enums when sqlx feature is enabled, fix sqlx incompatibilities * impl sqlx::postgres::PgHasArrayType for Snowflake * Try: derive Type for FriendSourceFlags, GuildFolder * Try: Derive FromRow, Type for DefaultReaction * Try: Derive Type for CustomStatus * Try: Derive Type, FromRow for Tag * Replace conditional compiling of uNN/PgUNN with conditional compiled type alias * Fix: Conditional compiling errors and warnings * Bump: wasm-bindgen* crate versions Uncomment and update decode_token() Add missing `impl From for u64`, closes `From for u64` missing #550 Update README.md move up sending identify Revert d846ce9948ebb07988428250d6a02d2ea920a451 Bump version to v0.16.0 --- src/types/entities/template.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/entities/template.rs b/src/types/entities/template.rs index f95bb574..29e53689 100644 --- a/src/types/entities/template.rs +++ b/src/types/entities/template.rs @@ -6,7 +6,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::types::{ - Shared, entities::{Guild, User}, utils::Snowflake, Shared, From 239f994841549bd8a05a2843342c59eaa3b61c40 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 21:26:19 +0200 Subject: [PATCH 09/23] Add Clone derive to GatewayRequestGuildMembers --- src/types/events/request_members.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types/events/request_members.rs b/src/types/events/request_members.rs index a6cffdf6..5228170d 100644 --- a/src/types/events/request_members.rs +++ b/src/types/events/request_members.rs @@ -5,7 +5,7 @@ use crate::types::{events::WebSocketEvent, Snowflake}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, WebSocketEvent)] +#[derive(Debug, Deserialize, Serialize, Default, WebSocketEvent, Clone)] /// See pub struct GatewayRequestGuildMembers { pub guild_id: Snowflake, @@ -16,4 +16,3 @@ pub struct GatewayRequestGuildMembers { pub user_ids: Option, pub nonce: Option, } - From 0eea35a59a0c92b98b0ae8bba0b5c4aa263a9864 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:39:04 +0200 Subject: [PATCH 10/23] User routes update (#537) * feat: Add UserProfile and other types * api: re-do a large part of the users api * feat: add modify user profile * feat: delete and disable user endpoints * feat: modify email and verify email endpoints * feat!: add discriminator parameter to get_user_by_username * feat!: add get_user_profile query string schema * chore: add integration expire behavior * feat: add get_pomelo_suggestions and get_pomelo_eligibility * feat: add create_pomelo_migration * fix: rustdoc lints * feat: recent_mentions endpoints Adds GET /users/@me/mentions and DELETE /users/@me/mentions/{message.id} * feat: add get_user_harvest & create_user_harvest Also adds /types/entities/harvest.rs, types for Harvest * feat: user notes endpoints Adds: get_user_notes, get_user_note, set_user_note * feat: add #545 and #546 Adds the RECENT_MENTION_DELETE and USER_NOTE_UPDATE gateway events. The events can be accessed at: message.recent_mention_delete & user.note_update * feat: add authorize_connection Also adds: src/types/entities/connection.rs, Connection and PublicConnection, src/api/users/connections.rs * feat: add rest* of Connections api * The only thing not added yet is create_domain_connection, because it uses errors in a funky way adds: - create_connection_callback - create_contact_sync_connection - get_connections - refresh_connection - modify_connection - delete_connection - get_connection_access_token - get_connection_subreddits + related schema for all those routes, and some supporting types * feat: add connected_accounts to UserProfile * feat: add affinities * feat: add get_premium_usage endpoint note: not fully tested; I do not have an account with premium * cliipy my arch nemesis strikes again * aa * feat: add create_domain_connection * feat: add get_burst_credits * grumble grumble * clippy * fix READY deserialization error on spacebar * fix a deserialization error on Spacebar See spacebarchat/server#1188 A deserialization error was happening with get_user_profile, where pronouns should have been serialized as an empty string, but were instead serialized as null. * skip serializing None query parameters * add test for get_user_profile * apparently Sb does not implement users/@me/notes * add some tests, minor connection updates - Document that create_domain_connection is unimplemented on Spacebar - Add Discord connection type - Change ConnectionType::array() into ConnectionType::vector() - returning a fixed size array is dubious, since it'll likely be expanded. Returning a vector is easier keep up to variable length - Add ConnectionType::discord_vector() and ConnectionType::spacebar_vector() to return a vector ConnectionTypes available on the respective server backends - add tests test_modify_user_profile, test_disable_user, test_get_user_note, test_set_user_note, test_get_user_affinities, test_get_guild_affinities, test_get_connections Note: connections are hard to test, since they require secrets / an external service's account * minor pre merge changes - add some extra doc comments - and into_public() for connection - remove a todo that is no longer valid --- src/api/auth/login.rs | 2 +- src/api/auth/mod.rs | 2 +- src/api/users/connections.rs | 391 +++++++++++++++ src/api/users/mod.rs | 2 + src/api/users/users.rs | 806 ++++++++++++++++++++++++++++-- src/gateway/events.rs | 3 + src/gateway/gateway.rs | 3 + src/types/entities/connection.rs | 300 +++++++++++ src/types/entities/guild.rs | 13 +- src/types/entities/harvest.rs | 97 ++++ src/types/entities/integration.rs | 33 +- src/types/entities/mod.rs | 4 + src/types/entities/user.rs | 650 +++++++++++++++++++++++- src/types/events/message.rs | 9 + src/types/events/session.rs | 2 +- src/types/events/user.rs | 22 + src/types/schema/user.rs | 358 ++++++++++++- tests/common/mod.rs | 8 +- tests/user.rs | 192 ++++++- 19 files changed, 2846 insertions(+), 51 deletions(-) create mode 100644 src/api/users/connections.rs create mode 100644 src/types/entities/connection.rs create mode 100644 src/types/entities/harvest.rs diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 1af19e05..4d8cbce6 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -48,7 +48,7 @@ impl Instance { >>>>>>> 03f1e7d (Refactor / fix login and register (#495)) user.settings = login_result.settings; - let object = User::get(&mut user, None).await?; + let object = User::get_current(&mut user).await?; *user.object.write().unwrap() = object; let mut identify = GatewayIdentifyPayload::common(); diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index 7aaf13f5..cdc41ba8 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -31,7 +31,7 @@ impl Instance { ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; >>>>>>> 03f1e7d (Refactor / fix login and register (#495)) - let object = User::get(&mut user, None).await?; + let object = User::get_current(&mut user).await?; let settings = User::get_settings(&mut user).await?; *user.object.write().unwrap() = object; diff --git a/src/api/users/connections.rs b/src/api/users/connections.rs new file mode 100644 index 00000000..29c40df1 --- /dev/null +++ b/src/api/users/connections.rs @@ -0,0 +1,391 @@ +use futures_util::FutureExt; +use reqwest::Client; + +use crate::{ + errors::{ChorusError, ChorusResult}, + instance::ChorusUser, + ratelimiter::ChorusRequest, + types::{ + AuthorizeConnectionReturn, AuthorizeConnectionSchema, Connection, ConnectionSubreddit, + ConnectionType, CreateConnectionCallbackSchema, CreateContactSyncConnectionSchema, + CreateDomainConnectionError, CreateDomainConnectionReturn, GetConnectionAccessTokenReturn, + LimitType, ModifyConnectionSchema, + }, +}; + +impl ChorusUser { + /// Fetches a url that can be used for authorizing a new connection. + /// + /// The user should then visit the url and authenticate to create the connection. + /// + /// # Notes + /// This route seems to be preferred by the official infrastructure (client) to + /// [Self::create_connection_callback]. + /// + /// # Reference + /// See + /// + /// Note: it doesn't seem to be actually unauthenticated + pub async fn authorize_connection( + &mut self, + connection_type: ConnectionType, + query_parameters: AuthorizeConnectionSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .get(format!( + "{}/connections/{}/authorize", + self.belongs_to.read().unwrap().urls.api, + connection_type_string + )) + // Note: ommiting this header causes a 401 Unauthorized, + // even though discord.sex mentions it as unauthenticated + .header("Authorization", self.token()) + .query(&query_parameters); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request + .deserialize_response::(self) + .await + .map(|response| response.url) + } + + /// Creates a new connection for the current user. + /// + /// # Notes + /// The official infrastructure (client) prefers the route + /// [Self::authorize_connection] to this one. + /// + /// # Reference + /// See + // TODO: When is this called? When should it be used over authorize_connection? + pub async fn create_connection_callback( + &mut self, + connection_type: ConnectionType, + json_schema: CreateConnectionCallbackSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .post(format!( + "{}/connections/{}/callback", + self.belongs_to.read().unwrap().urls.api, + connection_type_string + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Creates a new contact sync connection for the current user. + /// + /// # Notes + /// To create normal connection types, see [Self::authorize_connection] and + /// [Self::create_connection_callback] + /// + /// # Reference + /// See + pub async fn create_contact_sync_connection( + &mut self, + connection_account_id: &String, + json_schema: CreateContactSyncConnectionSchema, + ) -> ChorusResult { + let request = Client::new() + .put(format!( + "{}/users/@me/connections/contacts/{}", + self.belongs_to.read().unwrap().urls.api, + connection_account_id + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Creates a new domain connection for the current user. + /// + /// This route has two possible successful return values: + /// [CreateDomainConnectionReturn::Ok] and [CreateDomainConnectionReturn::ProofNeeded] + /// + /// To properly handle both, please see their respective documentation pages. + /// + /// # Notes + /// To create normal connection types, see [Self::authorize_connection] and + /// [Self::create_connection_callback] + /// + /// As of 2024/08/21, Spacebar does not yet implement this endpoint. + /// + /// # Examples + /// ```no_run + /// let domain = "example.com".to_string(); + /// + /// let user: ChorusUser; // Get this by registering / logging in + /// + /// let result = user.create_domain_connection(&domain).await; + /// + /// if let Ok(returned) = result { + /// match returned { + /// CreateDomainConnectionReturn::ProofNeeded(proof) => { + /// println!("Additional proof needed!"); + /// println!("Either:"); + /// println!(""); + /// println!("- create a DNS TXT record with the name _discord.{domain} and content {proof}"); + /// println!("or"); + /// println!("- create a file at https://{domain}/.well-known/discord with the content {proof}"); + /// // Once the user has added the proof, retry calling the endpoint + /// } + /// CreateDomainConnectionReturn::Ok(connection) => { + /// println!("Successfulyl created connection! {:?}", connection); + /// } + /// } + /// } else { + /// println!("Failed to create connection: {:?}", result); + /// } + /// ``` + /// + /// # Reference + /// See + pub async fn create_domain_connection( + &mut self, + domain: &String, + ) -> ChorusResult { + let request = Client::new() + .post(format!( + "{}/users/@me/connections/domain/{}", + self.belongs_to.read().unwrap().urls.api, + domain + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + let result = chorus_request + .deserialize_response::(self) + .await; + + if let Ok(connection) = result { + return Ok(CreateDomainConnectionReturn::Ok(connection)); + } + + let error = result.err().unwrap(); + + if let ChorusError::ReceivedErrorCode { + error_code, + error: ref error_string, + } = error + { + if error_code == 400 { + let try_deserialize: Result = + serde_json::from_str(error_string); + + if let Ok(deserialized_error) = try_deserialize { + return Ok(CreateDomainConnectionReturn::ProofNeeded( + deserialized_error.proof, + )); + } + } + } + + Err(error) + } + + /// Fetches the current user's [Connection]s + /// + /// # Reference + /// See + pub async fn get_connections(&mut self) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/connections", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Refreshes a local user's [Connection]. + /// + /// # Reference + /// See + pub async fn refresh_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult<()> { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .post(format!( + "{}/users/@me/connections/{}/{}/refresh", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } + + /// Changes settings on a local user's [Connection]. + /// + /// # Notes + /// Not all connection types support all parameters. + /// + /// # Reference + /// See + pub async fn modify_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + json_schema: ModifyConnectionSchema, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .patch(format!( + "{}/users/@me/connections/{}/{}", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()) + .json(&json_schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Deletes a local user's [Connection]. + /// + /// # Reference + /// See + pub async fn delete_connection( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult<()> { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .delete(format!( + "{}/users/@me/connections/{}/{}", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } + + /// Returns a new access token for the given connection. + /// + /// Only available for [ConnectionType::Twitch], [ConnectionType::YouTube] and [ConnectionType::Spotify] connections. + /// + /// # Reference + /// See + pub async fn get_connection_access_token( + &mut self, + connection_type: ConnectionType, + connection_account_id: &String, + ) -> ChorusResult { + let connection_type_string = serde_json::to_string(&connection_type) + .expect("Failed to serialize connection type!") + .replace('"', ""); + + let request = Client::new() + .get(format!( + "{}/users/@me/connections/{}/{}/access-token", + self.belongs_to.read().unwrap().urls.api, + connection_type_string, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request + .deserialize_response::(self) + .await + .map(|res| res.access_token) + } + + /// Fetches a list of [subreddits](crate::types::ConnectionSubreddit) + /// the connected account moderates. + /// + /// Only available for [ConnectionType::Reddit] connections. + /// + /// # Reference + /// See + pub async fn get_connection_subreddits( + &mut self, + connection_account_id: &String, + ) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/connections/reddit/{}/subreddits", + self.belongs_to.read().unwrap().urls.api, + connection_account_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } +} diff --git a/src/api/users/mod.rs b/src/api/users/mod.rs index b11772ab..702233cb 100644 --- a/src/api/users/mod.rs +++ b/src/api/users/mod.rs @@ -4,11 +4,13 @@ #![allow(unused_imports)] pub use channels::*; +pub use connections::*; pub use guilds::*; pub use relationships::*; pub use users::*; pub mod channels; +pub mod connections; pub mod guilds; pub mod relationships; pub mod users; diff --git a/src/api/users/users.rs b/src/api/users/users.rs index 3f43efbf..b8ab8d6a 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -2,7 +2,10 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use std::sync::{Arc, RwLock}; +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; use reqwest::Client; use serde_json::to_string; @@ -11,22 +14,69 @@ use crate::{ errors::{ChorusError, ChorusResult}, instance::{ChorusUser, Instance}, ratelimiter::ChorusRequest, - types::{LimitType, User, UserModifySchema, UserSettings}, + types::{ + AuthorizeConnectionSchema, BurstCreditsInfo, ConnectionType, CreateUserHarvestSchema, + DeleteDisableUserSchema, GetPomeloEligibilityReturn, GetPomeloSuggestionsReturn, + GetRecentMentionsSchema, GetUserProfileSchema, GuildAffinities, Harvest, + HarvestBackendType, LimitType, ModifyUserNoteSchema, PremiumUsage, PublicUser, Snowflake, + User, UserAffinities, UserModifyProfileSchema, UserModifySchema, UserNote, UserProfile, + UserProfileMetadata, UserSettings, VerifyUserEmailChangeResponse, + VerifyUserEmailChangeSchema, + }, }; impl ChorusUser { - /// Gets a user by id, or if the id is None, gets the current user. + /// Gets the local / current user. + /// + /// # Notes + /// This function is a wrapper around [`User::get_current`]. + /// + /// # Reference + /// See + pub async fn get_current_user(&mut self) -> ChorusResult { + User::get_current(self).await + } + + /// Gets a non-local user by their id /// /// # Notes /// This function is a wrapper around [`User::get`]. /// /// # Reference - /// See and - /// - pub async fn get_user(&mut self, id: Option<&String>) -> ChorusResult { + /// See + pub async fn get_user(&mut self, id: Snowflake) -> ChorusResult { User::get(self, id).await } + /// Gets a non-local user by their unique username. + /// + /// As of 2024/07/28, Spacebar does not yet implement this endpoint. + /// + /// If fetching with a pomelo username, discriminator should be set to None. + /// + /// This route also permits fetching users with their old pre-pomelo username#discriminator + /// combo. + /// + /// Note: + /// + /// "Unless the target user is a bot, you must be able to add + /// the user as a friend to resolve them by username. + /// + /// Due to this restriction, you are not able to resolve your own username." + /// + /// # Notes + /// This function is a wrapper around [`User::get_by_username`]. + /// + /// # Reference + /// See + pub async fn get_user_by_username( + &mut self, + username: &String, + discriminator: Option<&String>, + ) -> ChorusResult { + User::get_by_username(self, username, discriminator).await + } + /// Gets the user's settings. /// /// # Notes @@ -44,7 +94,6 @@ impl ChorusUser { /// # Reference /// See pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult { - // See , note 1 let requires_current_password = modify_schema.username.is_some() || modify_schema.discriminator.is_some() @@ -71,39 +120,583 @@ impl ChorusUser { chorus_request.deserialize_response::(self).await } - /// Deletes the user from the Instance. + /// Disables the current user's account. + /// + /// Invalidates all active tokens. + /// + /// Requires the user's current password (if any) + /// + /// # Notes + /// Requires MFA /// /// # Reference - /// See - pub async fn delete(mut self) -> ChorusResult<()> { + /// See + pub async fn disable(&mut self, schema: DeleteDisableUserSchema) -> ChorusResult<()> { + let request = Client::new() + .post(format!( + "{}/users/@me/disable", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()) + .json(&schema); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request.handle_request_as_result(self).await + } + + /// Deletes the current user from the Instance. + /// + /// Requires the user's current password (if any) + /// + /// # Notes + /// Requires MFA + /// + /// # Reference + /// See + pub async fn delete(&mut self, schema: DeleteDisableUserSchema) -> ChorusResult<()> { let request = Client::new() .post(format!( "{}/users/@me/delete", self.belongs_to.read().unwrap().urls.api )) .header("Authorization", self.token()) + .json(&schema); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request.handle_request_as_result(self).await + } + + /// Gets a user's profile object by their id. + /// + /// This endpoint requires one of the following: + /// + /// - The other user is a bot + /// - The other user shares a mutual guild with the current user + /// - The other user is a friend of the current user + /// - The other user is a friend suggestion of the current user + /// - The other user has an outgoing friend request to the current user + /// + /// # Notes + /// This function is a wrapper around [`User::get_profile`]. + /// + /// # Reference + /// See + pub async fn get_user_profile( + &mut self, + id: Snowflake, + query_parameters: GetUserProfileSchema, + ) -> ChorusResult { + User::get_profile(self, id, query_parameters).await + } + + /// Modifies the current user's profile. + /// + /// Returns the updated [UserProfileMetadata]. + /// + /// # Notes + /// This function is a wrapper around [`User::modify_profile`]. + /// + /// # Reference + /// See + pub async fn modify_profile( + &mut self, + schema: UserModifyProfileSchema, + ) -> ChorusResult { + User::modify_profile(self, schema).await + } + + /// Initiates the email change process. + /// + /// Sends a verification code to the current user's email. + /// + /// Should be followed up with [Self::verify_email_change] + /// + /// # Reference + /// See + pub async fn initiate_email_change(&mut self) -> ChorusResult<()> { + let request = Client::new() + .put(format!( + "{}/users/@me/email", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request.handle_request_as_result(self).await + } + + /// Verifies a code sent to change the current user's email. + /// + /// This endpoint returns a token which can be used with [Self::modify] + /// to set a new email address (email_token). + /// + /// # Notes + /// Should be the follow-up to [Self::initiate_email_change] + /// + /// As of 2024/08/08, Spacebar does not yet implement this endpoint. + // FIXME: Does this mean PUT users/@me/email is different? + /// + /// # Reference + /// See + pub async fn verify_email_change( + &mut self, + schema: VerifyUserEmailChangeSchema, + ) -> ChorusResult { + let request = Client::new() + .post(format!( + "{}/users/@me/email/verify-code", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()) + .json(&schema); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request + .deserialize_response::(self) + .await + } + + /// Returns a suggested unique username based on the current user's username. + /// + /// # Notes: + /// "This endpoint is used during the pomelo migration flow. + /// + /// The user must be in the rollout to use this endpoint." + /// + /// If a user has already migrated, this endpoint will likely return a 401 Unauthorized + /// ([ChorusError::NoPermission]) + /// + /// As of 2024/08/08, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn get_pomelo_suggestions(&mut self) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/pomelo-suggestions", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request + .deserialize_response::(self) + .await + .map(|returned| returned.username) + } + + /// Checks whether a unique username is available. + /// + /// Returns whether the username is not taken yet. + /// + /// # Notes + /// As of 2024/08/08, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn get_pomelo_eligibility(&mut self, username: &String) -> ChorusResult { + let request = Client::new() + .post(format!( + "{}/users/@me/pomelo-attempt", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()) + // FIXME: should we create a type for this? + .body(format!(r#"{{ "username": {:?} }}"#, username)) + .header("Content-Type", "application/json"); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request + .deserialize_response::(self) + .await + .map(|returned| !returned.taken) + } + + /// Migrates the user from the username#discriminator to the unique username system. + /// + /// Fires a [UserUpdate](crate::types::UserUpdate) gateway event. + /// + /// Updates [Self::object] to an updated representation returned by the server. + // FIXME: Is this appropriate behaviour? + /// + /// # Notes + /// "This endpoint is used during the pomelo migration flow. + /// + /// The user must be in the rollout to use this endpoint." + /// + /// If a user has already migrated, this endpoint will likely return a 401 Unauthorized + /// ([ChorusError::NoPermission]) + // + /// As of 2024/08/08, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn create_pomelo_migration(&mut self, username: &String) -> ChorusResult<()> { + let request = Client::new() + .post(format!( + "{}/users/@me/pomelo", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()) + // FIXME: should we create a type for this? + .body(format!(r#"{{ "username": {:?} }}"#, username)) .header("Content-Type", "application/json"); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + let result = chorus_request.deserialize_response::(self).await; + + // FIXME: Does UserUpdate do this automatically? or would a user need to manually observe ChorusUser::object + if let Ok(new_object) = result { + *self.object.write().unwrap() = new_object; + return ChorusResult::Ok(()); + } + + ChorusResult::Err(result.err().unwrap()) + } + + /// Fetches a list of [Message](crate::types::Message)s that the current user has been + /// mentioned in during the last 7 days. + /// + /// # Notes + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn get_recent_mentions( + &mut self, + query_parameters: GetRecentMentionsSchema, + ) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/mentions", + self.belongs_to.read().unwrap().urls.api + )) + .header("Authorization", self.token()) + .query(&query_parameters); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request + .deserialize_response::>(self) + .await + } + + /// Acknowledges a message the current user has been mentioned in. + /// + /// Fires a `RecentMentionDelete` gateway event. (Note: yet to be implemented in chorus, see [#545](https://github.com/polyphony-chat/chorus/issues/545)) + /// + /// # Notes + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn delete_recent_mention(&mut self, message_id: Snowflake) -> ChorusResult<()> { + let request = Client::new() + .delete(format!( + "{}/users/@me/mentions/{}", + self.belongs_to.read().unwrap().urls.api, + message_id + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(self).await + } + + /// If it exists, returns the most recent [Harvest] (personal data harvest request). + /// + /// To create a new [Harvest], see [Self::create_harvest]. + /// + /// # Notes + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// # Reference + /// See + pub async fn get_harvest(&mut self) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/harvest", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + // Manual handling, because a 204 with no harvest is a success state + // TODO: Maybe make this a method on ChorusRequest if we need it a lot + let response = chorus_request.send_request(self).await?; + log::trace!("Got response: {:?}", response); + + if response.status() == http::StatusCode::NO_CONTENT { + return Ok(None); + } + + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + let object = match serde_json::from_str::(&response_text) { + Ok(object) => object, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + }; + Ok(Some(object)) + } + + /// Creates a personal data harvest request ([Harvest]) for the current user. + /// + /// # Notes + /// To fetch the latest existing harvest, see [Self::get_harvest]. + /// + /// Invalid options in the backends array are ignored. + /// + /// If the array is empty (after ignoring), it requests all [HarvestBackendType]s. + /// + /// As of 2024/08/09, Spacebar does not yet implement this endpoint. (Or data harvesting) + /// + /// # Reference + /// See + pub async fn create_harvest( + &mut self, + backends: Vec, + ) -> ChorusResult { + let schema = if backends.is_empty() { + CreateUserHarvestSchema { backends: None } + } else { + CreateUserHarvestSchema { + backends: Some(backends), + } + }; + + let request = Client::new() + .post(format!( + "{}/users/@me/harvest", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()) + .json(&schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Returns a mapping of user IDs ([Snowflake]s) to notes ([String]s) for the current user. + /// + /// # Notes + /// As of 2024/08/21, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn get_user_notes(&mut self) -> ChorusResult> { + let request = Client::new() + .get(format!( + "{}/users/@me/notes", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + let chorus_request = ChorusRequest { request, limit_type: LimitType::default(), }; - chorus_request.handle_request_as_result(&mut self).await + + chorus_request.deserialize_response(self).await + } + + /// Fetches the note ([UserNote]) for the given user. + /// + /// If the current user has no note for the target, this endpoint + /// returns `Err(NotFound { error: "{\"message\": \"Unknown User\", \"code\": 10013}" })` + /// + /// # Notes + /// This function is a wrapper around [`User::get_note`]. + /// + /// # Reference + /// See + pub async fn get_user_note(&mut self, target_user_id: Snowflake) -> ChorusResult { + User::get_note(self, target_user_id).await + } + + /// Sets the note for the given user. + /// + /// Fires a `UserNoteUpdate` gateway event. (Note: yet to be implemented in chorus, see [#546](https://github.com/polyphony-chat/chorus/issues/546)) + /// + /// # Notes + /// This function is a wrapper around [`User::set_note`]. + /// + /// # Reference + /// See + pub async fn set_user_note( + &mut self, + target_user_id: Snowflake, + note: Option, + ) -> ChorusResult<()> { + User::set_note(self, target_user_id, note).await + } + + /// Fetches the current user's affinity scores for other users. + /// + /// (Affinity scores are a measure of how likely a user is to be friends with another user.) + /// + /// # Reference + /// See + pub async fn get_user_affinities(&mut self) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/affinities/users", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Fetches the current user's affinity scores for their joined guilds. + /// + /// # Reference + /// See + pub async fn get_guild_affinities(&mut self) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/affinities/guilds", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Fetches the current user's usage of various premium perks ([PremiumUsage] object). + /// + /// The local user must have premium (nitro), otherwise the request will fail + /// with a 404 NotFound error and the message {"message": "Premium usage not available", "code": 10084}. + /// + /// # Notes + /// As of 2024/08/16, Spacebar does not yet implement this endpoint. + /// + /// # Reference + /// See + pub async fn get_premium_usage(&mut self) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/premium-usage", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await + } + + /// Fetches info about the current user's burst credits + /// (how many are remaining, when they will replenish). + /// + /// Burst credits are used to create burst reactions. + /// + /// # Notes + /// As of 2024/08/18, Spacebar does not yet implement this endpoint. + pub async fn get_burst_credits(&mut self) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/burst-credits", + self.belongs_to.read().unwrap().urls.api, + )) + .header("Authorization", self.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(self).await } } impl User { - /// Gets a user by id, or if the id is None, gets the current user. + /// Gets the local / current user. /// /// # Reference - /// See and - /// - pub async fn get(user: &mut ChorusUser, id: Option<&String>) -> ChorusResult { + /// See + pub async fn get_current(user: &mut ChorusUser) -> ChorusResult { let url_api = user.belongs_to.read().unwrap().urls.api.clone(); - let url = if id.is_none() { - format!("{}/users/@me", url_api) - } else { - format!("{}/users/{}", url_api, id.unwrap()) + let url = format!("{}/users/@me", url_api); + let request = reqwest::Client::new() + .get(url) + .header("Authorization", user.token()); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, }; + chorus_request.deserialize_response::(user).await + } + + /// Gets a non-local user by their id + /// + /// # Reference + /// See + pub async fn get(user: &mut ChorusUser, id: Snowflake) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); + let url = format!("{}/users/{}", url_api, id); let request = reqwest::Client::new() .get(url) .header("Authorization", user.token()); @@ -111,16 +704,54 @@ impl User { request, limit_type: LimitType::Global, }; - match chorus_request.send_request(user).await { - Ok(result) => { - let result_text = result.text().await.unwrap(); - Ok(serde_json::from_str::(&result_text).unwrap()) - } - Err(e) => Err(e), + chorus_request + .deserialize_response::(user) + .await + } + + /// Gets a user by their unique username. + /// + /// As of 2024/07/28, Spacebar does not yet implement this endpoint. + /// + /// If fetching with a pomelo username, discriminator should be set to None. + /// + /// This route also permits fetching users with their old pre-pomelo username#discriminator + /// combo. + /// + /// Note: + /// + /// "Unless the target user is a bot, you must be able to add + /// the user as a friend to resolve them by username. + /// + /// Due to this restriction, you are not able to resolve your own username." + /// + /// # Reference + /// See + pub async fn get_by_username( + user: &mut ChorusUser, + username: &String, + discriminator: Option<&String>, + ) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); + let url = format!("{}/users/username/{username}", url_api); + let mut request = reqwest::Client::new() + .get(url) + .header("Authorization", user.token()); + + if let Some(some_discriminator) = discriminator { + request = request.query(&[("discriminator", some_discriminator)]); } + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await } - /// Gets the user's settings. + /// Gets the current user's settings. /// /// # Reference /// See @@ -133,13 +764,122 @@ impl User { request, limit_type: LimitType::Global, }; - match chorus_request.send_request(user).await { - Ok(result) => { - let result_text = result.text().await.unwrap(); - Ok(serde_json::from_str(&result_text).unwrap()) - } - Err(e) => Err(e), - } + chorus_request + .deserialize_response::(user) + .await + } + + /// Gets a user's profile object by their id. + /// + /// This endpoint requires one of the following: + /// + /// - The other user is a bot + /// - The other user shares a mutual guild with the current user + /// - The other user is a friend of the current user + /// - The other user is a friend suggestion of the current user + /// - The other user has an outgoing friend request to the current user + /// + /// # Reference + /// See + pub async fn get_profile( + user: &mut ChorusUser, + id: Snowflake, + query_parameters: GetUserProfileSchema, + ) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); + let request: reqwest::RequestBuilder = Client::new() + .get(format!("{}/users/{}/profile", url_api, id)) + .header("Authorization", user.token()) + .query(&query_parameters); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await + } + + /// Modifies the current user's profile. + /// + /// Returns the updated [UserProfileMetadata]. + /// + /// # Reference + /// See + pub async fn modify_profile( + user: &mut ChorusUser, + schema: UserModifyProfileSchema, + ) -> ChorusResult { + let url_api = user.belongs_to.read().unwrap().urls.api.clone(); + let request: reqwest::RequestBuilder = Client::new() + .patch(format!("{}/users/@me/profile", url_api)) + .header("Authorization", user.token()) + .json(&schema); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::(user) + .await + } + + /// Fetches the note ([UserNote]) for the given user. + /// + /// If the current user has no note for the target, this endpoint + /// returns `Err(NotFound { error: "{\"message\": \"Unknown User\", \"code\": 10013}" })` + /// + /// # Reference + /// See + pub async fn get_note( + user: &mut ChorusUser, + target_user_id: Snowflake, + ) -> ChorusResult { + let request = Client::new() + .get(format!( + "{}/users/@me/notes/{}", + user.belongs_to.read().unwrap().urls.api, + target_user_id + )) + .header("Authorization", user.token()); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.deserialize_response(user).await + } + + /// Sets the note for the given user. + /// + /// Fires a `UserNoteUpdate` gateway event. (Note: yet to be implemented in chorus, see [#546](https://github.com/polyphony-chat/chorus/issues/546)) + /// + /// # Reference + /// See + pub async fn set_note( + user: &mut ChorusUser, + target_user_id: Snowflake, + note: Option, + ) -> ChorusResult<()> { + let schema = ModifyUserNoteSchema { note }; + + let request = Client::new() + .put(format!( + "{}/users/@me/notes/{}", + user.belongs_to.read().unwrap().urls.api, + target_user_id + )) + .header("Authorization", user.token()) + .json(&schema); + + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + + chorus_request.handle_request_as_result(user).await } } <<<<<<< HEAD diff --git a/src/gateway/events.rs b/src/gateway/events.rs index 049434be..4663fe1a 100644 --- a/src/gateway/events.rs +++ b/src/gateway/events.rs @@ -69,12 +69,15 @@ pub struct Message { pub reaction_remove: Publisher, pub reaction_remove_all: Publisher, pub reaction_remove_emoji: Publisher, + pub recent_mention_delete: Publisher, pub ack: Publisher, } #[derive(Default, Debug)] pub struct User { pub update: Publisher, + pub connections_update: Publisher, + pub note_update: Publisher, pub guild_settings_update: Publisher, pub presence_update: Publisher, pub typing_start: Publisher, diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 976769d2..20f86407 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -404,6 +404,7 @@ impl Gateway { "MESSAGE_REACTION_REMOVE" => message.reaction_remove, // TODO "MESSAGE_REACTION_REMOVE_ALL" => message.reaction_remove_all, // TODO "MESSAGE_REACTION_REMOVE_EMOJI" => message.reaction_remove_emoji, // TODO + "RECENT_MENTION_DELETE" => message.recent_mention_delete, "MESSAGE_ACK" => message.ack, "PRESENCE_UPDATE" => user.presence_update, // TODO "RELATIONSHIP_ADD" => relationship.add, @@ -413,6 +414,8 @@ impl Gateway { "STAGE_INSTANCE_DELETE" => stage_instance.delete, "TYPING_START" => user.typing_start, "USER_UPDATE" => user.update, // TODO + "USER_CONNECTIONS_UPDATE" => user.connections_update, // TODO + "USER_NOTE_UPDATE" => user.note_update, "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, "VOICE_STATE_UPDATE" => voice.state_update, // TODO "VOICE_SERVER_UPDATE" => voice.server_update, diff --git a/src/types/entities/connection.rs b/src/types/entities/connection.rs new file mode 100644 index 00000000..e6421b07 --- /dev/null +++ b/src/types/entities/connection.rs @@ -0,0 +1,300 @@ +use std::{collections::HashMap, fmt::Display}; + +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +/// A 3rd party service connection to a user's account. +/// +/// # Reference +/// See +// TODO: Should (could) this type be Updateable and Composite? +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct Connection { + /// The id of the account on the 3rd party service + #[serde(rename = "id")] + pub connected_account_id: String, + + /// The service of the connected account + #[serde(rename = "type")] + pub connection_type: ConnectionType, + + /// The username of the connection account + pub name: String, + + /// If the connection is verified + pub verified: bool, + + /// Service specific metadata about the connection / connected account + // FIXME: Is there a better type? As far as I see the value is always encoded as a string + pub metadata: Option>, + pub metadata_visibility: ConnectionVisibilityType, + + /// If the connection if revoked + pub revoked: bool, + + // TODO: Add integrations + pub friend_sync: bool, + + /// Whether activities related to this connection will be shown in presence + pub show_activity: bool, + + /// Whether this connection has a corresponding 3rd party OAuth2 token + pub two_way_link: bool, + + /// Who can see this connection + pub visibility: ConnectionVisibilityType, + + /// The access token for the connection account + /// + /// Note: not included when fetching a user's connections via OAuth2 + pub access_token: Option, +} + +impl Connection { + /// Converts self info a [PublicConnection], forgetting private data + pub fn into_public(self: Connection) -> PublicConnection { + PublicConnection { + name: self.name, + verified: self.verified, + connection_type: self.connection_type, + connected_account_id: self.connected_account_id, + metadata: self.metadata, + } + } +} + +/// A partial / public [Connection] type. +/// +/// # Reference +/// See +// FIXME: Should (could) this type also be Updateable and Composite? +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct PublicConnection { + /// The id of the account on the 3rd party service + #[serde(rename = "id")] + pub connected_account_id: String, + + #[serde(rename = "type")] + pub connection_type: ConnectionType, + + /// The username of the connection account + pub name: String, + + /// If the connection is verified + pub verified: bool, + + /// Service specific metadata about the connection / connected account + // FIXME: Is there a better type? As far as I see the value is always encoded as a string + pub metadata: Option>, +} + +impl From for PublicConnection { + fn from(value: Connection) -> Self { + value.into_public() + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[serde(rename_all = "lowercase")] +/// A type of connection; the service the connection is for +/// +/// Note: this is subject to change, and the enum is likely non-exhaustive +/// +/// # Reference +/// See +pub enum ConnectionType { + #[serde(rename = "amazon-music")] + AmazonMusic, + /// Battle.net + BattleNet, + /// Bungie.net + Bungie, + /// Discord?'s contact sync + /// + /// (Not returned in Get User Profile or when fetching connections) + Contacts, + Crunchyroll, + /// Note: spacebar only + Discord, + Domain, + Ebay, + EpicGames, + Facebook, + GitHub, + Instagram, + LeagueOfLegends, + PayPal, + /// Playstation network + Playstation, + Reddit, + Roblox, + RiotGames, + /// Samsung Galaxy + /// + /// Users can no longer add this service + Samsung, + Spotify, + /// Users can no longer add this service + Skype, + Steam, + TikTok, + Twitch, + Twitter, + Xbox, + YouTube, +} + +impl Display for ConnectionType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + Self::AmazonMusic => f.write_str("Amazon Music"), + Self::BattleNet => f.write_str("Battle.net"), + Self::Bungie => f.write_str("Bungie.net"), + Self::Ebay => f.write_str("eBay"), + Self::EpicGames => f.write_str("Epic Games"), + Self::LeagueOfLegends => f.write_str("League of Legends"), + Self::Playstation => f.write_str("PlayStation Network"), + Self::RiotGames => f.write_str("Riot Games"), + Self::Samsung => f.write_str("Samsung Galaxy"), + _ => f.write_str(format!("{:?}", self).as_str()), + } + } +} + +impl ConnectionType { + /// Returns an vector of all the connection types + // API note: this could be an array, but it is subject to change. + pub fn vector() -> Vec { + vec![ + ConnectionType::AmazonMusic, + ConnectionType::BattleNet, + ConnectionType::Bungie, + ConnectionType::Contacts, + ConnectionType::Crunchyroll, + ConnectionType::Discord, + ConnectionType::Domain, + ConnectionType::Ebay, + ConnectionType::EpicGames, + ConnectionType::Facebook, + ConnectionType::GitHub, + ConnectionType::Instagram, + ConnectionType::LeagueOfLegends, + ConnectionType::PayPal, + ConnectionType::Playstation, + ConnectionType::Reddit, + ConnectionType::RiotGames, + ConnectionType::Samsung, + ConnectionType::Spotify, + ConnectionType::Skype, + ConnectionType::Steam, + ConnectionType::TikTok, + ConnectionType::Twitch, + ConnectionType::Twitter, + ConnectionType::Xbox, + ConnectionType::YouTube, + ] + } + + /// Returns an vector of all the connection types available on discord + pub fn discord_vector() -> Vec { + vec![ + ConnectionType::AmazonMusic, + ConnectionType::BattleNet, + ConnectionType::Bungie, + ConnectionType::Contacts, + ConnectionType::Crunchyroll, + ConnectionType::Domain, + ConnectionType::Ebay, + ConnectionType::EpicGames, + ConnectionType::Facebook, + ConnectionType::GitHub, + ConnectionType::Instagram, + ConnectionType::LeagueOfLegends, + ConnectionType::PayPal, + ConnectionType::Playstation, + ConnectionType::Reddit, + ConnectionType::RiotGames, + ConnectionType::Samsung, + ConnectionType::Spotify, + ConnectionType::Skype, + ConnectionType::Steam, + ConnectionType::TikTok, + ConnectionType::Twitch, + ConnectionType::Twitter, + ConnectionType::Xbox, + ConnectionType::YouTube, + ] + } + + /// Returns an vector of all the connection types available on spacebar + pub fn spacebar_vector() -> Vec { + vec![ + ConnectionType::BattleNet, + ConnectionType::Discord, + ConnectionType::EpicGames, + ConnectionType::Facebook, + ConnectionType::GitHub, + ConnectionType::Reddit, + ConnectionType::Spotify, + ConnectionType::Twitch, + ConnectionType::Twitter, + ConnectionType::Xbox, + ConnectionType::YouTube, + ] + } +} + +#[derive( + Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// # Reference +/// See +pub enum ConnectionVisibilityType { + /// Invisible to everyone except the user themselves + None = 0, + /// Visible to everyone + Everyone = 1, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[serde(rename_all = "lowercase")] +/// A type of two-way connection link +/// +/// # Reference +/// See +pub enum TwoWayLinkType { + /// The connection is linked via web + Web, + /// The connection is linked via mobile + Mobile, + /// The connection is linked via desktop + Desktop, +} + +impl Display for TwoWayLinkType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{:?}", self).as_str()) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +/// Defines a subreddit as fetched through a Reddit connection ([[ConnectionType::Reddit]]). +/// +/// # Reference +/// See +pub struct ConnectionSubreddit { + /// The subreddit's internal id, e.g. "t5_388p4" + pub id: String, + + /// How many reddit users follow the subreddit + pub subscribers: usize, + + /// The subreddit's relative url, e.g. "/r/discordapp/" + pub url: String, +} diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index 423339a1..91850ac2 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -452,7 +452,7 @@ pub enum VerificationLevel { #[cfg_attr(not(feature = "sqlx"), repr(u8))] #[cfg_attr(feature = "sqlx", repr(i16))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +/// See pub enum MFALevel { #[default] None = 0, @@ -476,7 +476,7 @@ pub enum MFALevel { #[cfg_attr(not(feature = "sqlx"), repr(u8))] #[cfg_attr(feature = "sqlx", repr(i16))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +/// See pub enum NSFWLevel { #[default] Default = 0, @@ -502,12 +502,19 @@ pub enum NSFWLevel { #[cfg_attr(not(feature = "sqlx"), repr(u8))] #[cfg_attr(feature = "sqlx", repr(i16))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] -/// See +// Note: Maybe rename this to GuildPremiumTier? +/// **Guild** premium (Boosting) tier +/// +/// See pub enum PremiumTier { #[default] + /// No server boost perks None = 0, + /// Level 1 server boost perks Tier1 = 1, + /// Level 2 server boost perks Tier2 = 2, + /// Level 3 server boost perks Tier3 = 3, } diff --git a/src/types/entities/harvest.rs b/src/types/entities/harvest.rs new file mode 100644 index 00000000..b747641a --- /dev/null +++ b/src/types/entities/harvest.rs @@ -0,0 +1,97 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::types::Snowflake; + +#[cfg(feature = "client")] +use crate::gateway::Updateable; + +// FIXME: Should this type be Composite? +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// A user's data harvest. +/// +/// # Reference +/// +/// See +pub struct Harvest { + pub harvest_id: Snowflake, + /// The id of the user being harvested + pub user_id: Snowflake, + /// How much the harvest has been processed + pub status: HarvestStatus, + /// The time the harvest was created + pub created_at: DateTime, + /// The time the harvest was last polled + pub polled_at: Option>, + /// The time the harvest was completed + pub completed_at: Option>, +} + +#[cfg(feature = "client")] +impl Updateable for Harvest { + #[cfg(not(tarpaulin_include))] + fn id(&self) -> Snowflake { + self.harvest_id + } +} + +#[derive( + Serialize_repr, + Deserialize_repr, + Debug, + Default, + Clone, + Eq, + PartialEq, + Hash, + Copy, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// Current status of a [Harvest] +/// +/// See and +pub enum HarvestStatus { + /// The harvest is queued and has not been started + Queued = 0, + /// The harvest is currently running / being processed + Running = 1, + /// The harvest has failed + Failed = 2, + /// The harvest has been completed successfully + Completed = 3, + #[default] + Unknown = 4, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash, Copy, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +/// A type of backend / service a harvest can be requested for. +/// +/// See and +pub enum HarvestBackendType { + /// All account information; + Accounts, + /// Actions the user has taken; + /// + /// Represented as "Your Activity" in the discord client + Analytics, + /// First-party embedded activity information; + /// + /// e.g.: Chess in the Park, Checkers in the Park, Poker Night 2.0; + /// Sketch Heads, Watch Together, Letter League, Land-io, Know What I Meme + Activities, + /// The user's messages + Messages, + /// Official Discord programes; + /// + /// e.g.: Partner, HypeSquad, Verified Server + Programs, + /// Guilds the user is a member of; + Servers, +} diff --git a/src/types/entities/integration.rs b/src/types/entities/integration.rs index 50a82819..8afec21f 100644 --- a/src/types/entities/integration.rs +++ b/src/types/entities/integration.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::types::{ entities::{Application, User}, @@ -24,7 +25,7 @@ pub struct Integration { pub syncing: Option, pub role_id: Option, pub enabled_emoticons: Option, - pub expire_behaviour: Option, + pub expire_behaviour: Option, pub expire_grace_period: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] pub user: Option>, @@ -51,6 +52,7 @@ pub struct IntegrationAccount { #[serde(rename_all = "snake_case")] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))] +/// See pub enum IntegrationType { #[default] Twitch, @@ -58,3 +60,32 @@ pub enum IntegrationType { Discord, GuildSubscription, } + +#[derive( + Serialize_repr, + Deserialize_repr, + Debug, + Default, + Clone, + Eq, + PartialEq, + Hash, + Copy, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// Defines the behaviour that is executed when a user's subscription to the integration expires. +/// +/// See +pub enum IntegrationExpireBehaviour { + #[default] + /// Remove the subscriber role from the user + RemoveRole = 0, + /// Kick the user from the guild + Kick = 1, +} + + diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index 545614cd..969d731a 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -8,9 +8,11 @@ pub use audit_log::*; pub use auto_moderation::*; pub use channel::*; pub use config::*; +pub use connection::*; pub use emoji::*; pub use guild::*; pub use guild_member::*; +pub use harvest::*; pub use integration::*; pub use invite::*; pub use message::*; @@ -49,9 +51,11 @@ mod audit_log; mod auto_moderation; mod channel; mod config; +mod connection; mod emoji; mod guild; mod guild_member; +mod harvest; mod integration; mod invite; mod message; diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index 866d66cf..9c7c3b9d 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -7,7 +7,8 @@ use crate::types::utils::Snowflake; use crate::{UInt32, UInt8}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_aux::prelude::deserialize_option_number_from_string; +use serde_aux::prelude::{deserialize_option_number_from_string, deserialize_default_from_null}; +use serde_repr::{Deserialize_repr, Serialize_repr}; use std::array::TryFromSliceError; use std::fmt::Debug; @@ -23,7 +24,7 @@ use crate::gateway::GatewayHandle; #[cfg(feature = "client")] use chorus_macros::{Composite, Updateable}; -use super::Emoji; +use super::{Emoji, GuildMember, PublicConnection}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] @@ -40,6 +41,8 @@ impl User { #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +/// # Reference +/// See pub struct User { pub id: Snowflake, pub username: String, @@ -58,8 +61,10 @@ pub struct User { #[serde(default)] #[serde(deserialize_with = "deserialize_option_number_from_string")] pub flags: Option, + pub premium: Option, + /// The type of premium (Nitro) a user has + pub premium_type: Option, pub premium_since: Option>, - pub premium_type: Option, pub pronouns: Option, pub public_flags: Option, pub banner: Option, @@ -67,13 +72,15 @@ pub struct User { pub theme_colors: Option, pub phone: Option, pub nsfw_allowed: Option, - pub premium: Option, pub purchased_flags: Option, pub premium_usage_flags: Option, pub disabled: Option, } #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] +/// A user's theme colors, as u32s representing hex color codes +/// +/// found in [UserProfileMetadata] pub struct ThemeColors { #[serde(flatten)] inner: (u32, u32), @@ -140,6 +147,8 @@ impl sqlx::Type for ThemeColors { } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +/// # Reference +/// See pub struct PublicUser { pub id: Snowflake, pub username: Option, @@ -151,7 +160,9 @@ pub struct PublicUser { pub pronouns: Option, pub bot: Option, pub bio: Option, - pub premium_type: Option, + /// The type of premium (Nitro) a user has + pub premium_type: Option, + /// The date the user's premium (Nitro) subscribtion started pub premium_since: Option>, pub public_flags: Option, } @@ -182,6 +193,8 @@ const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; bitflags::bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)] #[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))] + /// # Reference + /// See pub struct UserFlags: u64 { const DISCORD_EMPLOYEE = 1 << 0; const PARTNERED_SERVER_OWNER = 1 << 1; @@ -195,6 +208,7 @@ bitflags::bitflags! { const EARLY_SUPPORTER = 1 << 9; const TEAM_USER = 1 << 10; const TRUST_AND_SAFETY = 1 << 11; + /// Note: deprecated by Discord const SYSTEM = 1 << 12; const HAS_UNREAD_URGENT_MESSAGES = 1 << 13; const BUGHUNTER_LEVEL_2 = 1 << 14; @@ -206,14 +220,640 @@ bitflags::bitflags! { } } +#[derive( + Serialize_repr, + Deserialize_repr, + Debug, + Default, + Clone, + Eq, + PartialEq, + Hash, + Copy, + PartialOrd, + Ord, +)] +#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] +#[repr(u8)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +/// **User** premium (Nitro) type +/// +/// See +pub enum PremiumType { + #[default] + /// No Nitro + None = 0, + /// Nitro Classic + Tier1 = 1, + /// Nitro + Tier2 = 2, + /// Nitro Basic + Tier3 = 3, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// # Reference +/// See pub struct UserProfileMetadata { + /// The guild ID this profile applies to, if it is a guild profile. pub guild_id: Option, + /// The user's pronouns, up to 40 characters + #[serde(deserialize_with = "deserialize_default_from_null")] + // Note: spacebar will send this is as null, while it should be "" + // See issue 1188 pub pronouns: String, + /// The user's bio / description, up to 190 characters pub bio: Option, + /// The hash used to retrieve the user's banned from the CDN pub banner: Option, + /// Banner color encoded as an i32 representation of a hex color code pub accent_color: Option, + /// See [ThemeColors] pub theme_colors: Option, pub popout_animation_particle_type: Option, pub emoji: Option, } + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +/// A user's publically facing profile +/// +/// # Reference +/// See +pub struct UserProfile { + // TODO: add profile application object + pub user: PublicUser, + + #[serde(rename = "user_profile")] + pub profile_metadata: UserProfileMetadata, + + #[serde(default)] + pub badges: Vec, + + pub guild_member: Option, + + #[serde(rename = "guild_member_profile")] + pub guild_member_profile_metadata: Option, + + #[serde(default)] + pub guild_badges: Vec, + + /// The user's legacy username#discriminator, if existing and shown + pub legacy_username: Option, + + #[serde(default)] + pub mutual_guilds: Vec, + + #[serde(default)] + pub mutual_friends: Vec, + + pub mutual_friends_count: Option, + + pub connected_accounts: Vec, + + // TODO: Add application role connections! + /// The type of premium (Nitro) a user has + pub premium_type: Option, + /// The date the user's premium (Nitro) subscribtion started + pub premium_since: Option>, + /// The date the user's premium guild (Boosting) subscribtion started + pub premium_guild_since: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// Info about a badge on a user's profile ([UserProfile]) +/// +/// # Reference +/// See +/// +/// For a list of know badges, see +pub struct ProfileBadge { + /// The badge's unique id, e.g. "staff", "partner", "premium", ... + pub id: String, + /// Description of what the badge represents, e.g. "Discord Staff" + pub description: String, + /// An icon hash, to get the badge's icon from the CDN + pub icon: String, + /// A link (potentially used for href) for the badge. + /// + /// e.g.: + /// `"staff"` badge links to `"https://discord.com/company"` + /// `"certified_moderator"` links to `"https://discord.com/safety"` + pub link: Option, +} + +impl PartialEq for ProfileBadge { + fn eq(&self, other: &Self) -> bool { + // Note: does not include description, since it changes for some badges + // + // Think nitro "Subscriber since ...", "Server boosting since ..." + self.id.eq(&other.id) && self.icon.eq(&other.icon) && self.link.eq(&other.link) + } +} + +impl ProfileBadge { + /// Returns a badge representing the "staff" badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_staff() -> Self { + Self { + id: "staff".to_string(), + description: "Discord Staff".to_string(), + icon: "5e74e9b61934fc1f67c65515d1f7e60d".to_string(), + link: Some("https://discord.com/company".to_string()), + } + } + + /// Returns a badge representing the partnered server owner badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_partner() -> Self { + Self { + id: "partner".to_string(), + description: "Partnered Server Owner".to_string(), + icon: "3f9748e53446a137a052f3454e2de41e".to_string(), + link: Some("https://discord.com/partners".to_string()), + } + } + + /// Returns a badge representing the certified moderator badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_certified_moderator() -> Self { + Self { + id: "certified_moderator".to_string(), + description: "Moderator Programs Alumni".to_string(), + icon: "fee1624003e2fee35cb398e125dc479b".to_string(), + link: Some("https://discord.com/safety".to_string()), + } + } + + /// Returns a badge representing the hypesquad events badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_hypesquad() -> Self { + Self { + id: "hypesquad".to_string(), + description: "HypeSquad Events".to_string(), + icon: "bf01d1073931f921909045f3a39fd264".to_string(), + link: Some("https://support.discord.com/hc/en-us/articles/360035962891-Profile-Badges-101#h_01GM67K5EJ16ZHYZQ5MPRW3JT3".to_string()), + } + } + + /// Returns a badge representing the hypesquad bravery badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_hypesquad_bravery() -> Self { + Self { + id: "hypesquad_house_1".to_string(), + description: "HypeSquad Bravery".to_string(), + icon: "8a88d63823d8a71cd5e390baa45efa02".to_string(), + link: Some("https://discord.com/settings/hypesquad-online".to_string()), + } + } + + /// Returns a badge representing the hypesquad brilliance badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_hypesquad_brilliance() -> Self { + Self { + id: "hypesquad_house_2".to_string(), + description: "HypeSquad Brilliance".to_string(), + icon: "011940fd013da3f7fb926e4a1cd2e618".to_string(), + link: Some("https://discord.com/settings/hypesquad-online".to_string()), + } + } + + /// Returns a badge representing the hypesquad balance badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_hypesquad_balance() -> Self { + Self { + id: "hypesquad_house_3".to_string(), + description: "HypeSquad Balance".to_string(), + icon: "3aa41de486fa12454c3761e8e223442e".to_string(), + link: Some("https://discord.com/settings/hypesquad-online".to_string()), + } + } + + /// Returns a badge representing the bug hunter level 1 badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_bug_hunter_1() -> Self { + Self { + id: "bug_hunter_level_1".to_string(), + description: "Discord Bug Hunter".to_string(), + icon: "2717692c7dca7289b35297368a940dd0".to_string(), + link: Some( + "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" + .to_string(), + ), + } + } + + /// Returns a badge representing the bug hunter level 2 badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_bug_hunter_2() -> Self { + Self { + id: "bug_hunter_level_2".to_string(), + description: "Discord Bug Hunter".to_string(), + icon: "848f79194d4be5ff5f81505cbd0ce1e6".to_string(), + link: Some( + "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" + .to_string(), + ), + } + } + + /// Returns a badge representing the active developer badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_active_developer() -> Self { + Self { + id: "active_developer".to_string(), + description: "Active Developer".to_string(), + icon: "6bdc42827a38498929a4920da12695d9".to_string(), + link: Some( + "https://support-dev.discord.com/hc/en-us/articles/10113997751447?ref=badge" + .to_string(), + ), + } + } + + /// Returns a badge representing the early verified bot developer badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_early_verified_developer() -> Self { + Self { + id: "verified_developer".to_string(), + description: "Early Verified Bot Developer".to_string(), + icon: "6df5892e0f35b051f8b61eace34f4967".to_string(), + link: None, + } + } + + /// Returns a badge representing the early supporter badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_early_supporter() -> Self { + Self { + id: "early_supporter".to_string(), + description: "Early Supporter".to_string(), + icon: "7060786766c9c840eb3019e725d2b358".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the nitro subscriber badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_nitro() -> Self { + Self { + id: "premium".to_string(), + description: "Subscriber since 1 Jan 2015".to_string(), + icon: "2ba85e8026a8614b640c2837bcdfe21b".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 1 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_1() -> Self { + Self { + id: "guild_booster_lvl1".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "51040c70d4f20a921ad6674ff86fc95c".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 2 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_2() -> Self { + Self { + id: "guild_booster_lvl2".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "0e4080d1d333bc7ad29ef6528b6f2fb7".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 3 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_3() -> Self { + Self { + id: "guild_booster_lvl3".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "72bed924410c304dbe3d00a6e593ff59".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 4 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_4() -> Self { + Self { + id: "guild_booster_lvl4".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "df199d2050d3ed4ebf84d64ae83989f8".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 5 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_5() -> Self { + Self { + id: "guild_booster_lvl5".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "996b3e870e8a22ce519b3a50e6bdd52f".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 6 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_6() -> Self { + Self { + id: "guild_booster_lvl6".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "991c9f39ee33d7537d9f408c3e53141e".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 7 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_7() -> Self { + Self { + id: "guild_booster_lvl7".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "cb3ae83c15e970e8f3d410bc62cb8b99".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 8 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_8() -> Self { + Self { + id: "guild_booster_lvl8".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "7142225d31238f6387d9f09efaa02759".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the level 9 server boosting badge on Discord.com + /// + /// Note: The description updates for the start date + /// + /// # Reference + /// See + pub fn discord_server_boosting_9() -> Self { + Self { + id: "guild_booster_lvl9".to_string(), + description: "Server boosting since 1 Jan 2015".to_string(), + icon: "ec92202290b48d0879b7413d2dde3bab".to_string(), + link: Some("https://discord.com/settings/premium".to_string()), + } + } + + /// Returns a badge representing the legacy username badge on Discord.com + /// + /// # Reference + /// See + pub fn discord_legacy_username() -> Self { + Self { + id: "legacy_username".to_string(), + description: "Originally known as USERNAME".to_string(), + icon: "6de6d34650760ba5551a79732e98ed60".to_string(), + link: None, + } + } + + /// Returns a badge representing the legacy username badge on Discord.com, + /// with the provided username (which should already contain the #DISCRIM part) + /// + /// # Reference + /// See + pub fn discord_legacy_username_with_username(username: String) -> Self { + Self { + id: "legacy_username".to_string(), + description: format!("Originally known as {username}"), + icon: "6de6d34650760ba5551a79732e98ed60".to_string(), + link: None, + } + } + + /// Returns a badge representing the legacy username badge on Discord.com, + /// with the provided username and discriminator + /// + /// # Reference + /// See + pub fn discord_legacy_username_with_username_and_discriminator( + username: String, + discriminator: String, + ) -> Self { + Self { + id: "legacy_username".to_string(), + description: format!("Originally known as {username}#{discriminator}"), + icon: "6de6d34650760ba5551a79732e98ed60".to_string(), + link: None, + } + } + + /// Returns a badge representing the bot commands badge on Discord.com + /// + /// Note: This badge is only for bot accounts + /// + /// # Reference + /// See + pub fn discord_bot_commands() -> Self { + Self { + id: "bot_commands".to_string(), + description: "Supports Commands".to_string(), + icon: "6f9e37f9029ff57aef81db857890005e".to_string(), + link: Some( + "https://discord.com/blog/welcome-to-the-new-era-of-discord-apps?ref=badge" + .to_string(), + ), + } + } + + /// Returns a badge representing the bot automod badge on Discord.com + /// + /// Note: This badge is only for bot accounts + /// + /// # Reference + /// See + pub fn discord_bot_automod() -> Self { + Self { + id: "automod".to_string(), + description: "Uses AutoMod".to_string(), + icon: "f2459b691ac7453ed6039bbcfaccbfcd".to_string(), + link: None, + } + } + + /// Returns a badge representing the application guild subscription badge on Discord.com + /// + /// No idea where this badge could show up, but apparently it means a guild has an + /// application's premium + /// + /// # Reference + /// See + pub fn discord_application_guild_subscription() -> Self { + Self { + id: "application_guild_subscription".to_string(), + description: "This server has APPLICATION Premium".to_string(), + icon: "d2010c413a8da2208b7e4f35bd8cd4ac".to_string(), + link: None, + } + } +} + +/// Structure which shows a mutual guild with a user +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct MutualGuild { + pub id: Snowflake, + /// The user's nickname in the guild, if any + pub nick: Option, +} + +/// Structure which is returned by the [crate::instance::ChorusUser::get_user_note] endpoint. +/// +/// Note that [crate::instance::ChorusUser::get_user_notes] endpoint +/// returns a completely different structure; +// Specualation: this is probably how Discord stores notes internally +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct UserNote { + /// Actual note contents; max 256 characters + pub note: String, + /// The ID of the user the note is on + pub note_user_id: Snowflake, + /// The ID of the user who created the note (always the current user) + pub user_id: Snowflake, +} + +/// Structure which defines an affinity the local user has with another user. +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd)] +pub struct UserAffinity { + /// The other user's id + pub user_id: Snowflake, + /// The affinity score + pub affinity: f32, +} + +/// Structure which defines an affinity the local user has with a guild. +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd)] +pub struct GuildAffinity { + /// The guild's id + pub guild_id: Snowflake, + /// The affinity score + pub affinity: f32, +} + +/// Structure which defines the local user's premium perk usage. +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PremiumUsage { + /// Number of Nitro stickers the user has sent + pub nitro_sticker_sends: PremiumUsageData, + /// Number of animated emojis the user has sent + pub total_animated_emojis: PremiumUsageData, + /// Number of global emojis the user has sent + pub total_global_emojis: PremiumUsageData, + /// Number of large uploads the user has made + pub total_large_uploads: PremiumUsageData, + /// Number of times the user has streamed in HD + pub total_hd_streams: PremiumUsageData, + /// Number of hours the user has streamed in HD + pub hd_hours_streamed: PremiumUsageData, +} + +/// Structure for the data in [PremiumUsage]. +/// +/// Currently only contains the number of uses of a premium perk. +/// +/// # Reference +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct PremiumUsageData { + /// Total number of uses for this perk + pub value: usize, +} + +impl From for usize { + fn from(value: PremiumUsageData) -> Self { + value.value + } +} + +impl From for PremiumUsageData { + fn from(value: usize) -> Self { + PremiumUsageData { value } + } +} diff --git a/src/types/events/message.rs b/src/types/events/message.rs index 1b855dfc..d2ca3082 100644 --- a/src/types/events/message.rs +++ b/src/types/events/message.rs @@ -149,6 +149,15 @@ pub struct MessageReactionRemoveEmoji { pub emoji: Emoji, } +#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, WebSocketEvent)] +/// Sent when a message that mentioned the current user in the last week is acknowledged and deleted. +/// +/// # Reference +/// See +pub struct RecentMentionDelete { + pub message_id: Snowflake, +} + #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] /// Officially Undocumented /// diff --git a/src/types/events/session.rs b/src/types/events/session.rs index a76ebc3c..8adaa336 100644 --- a/src/types/events/session.rs +++ b/src/types/events/session.rs @@ -30,7 +30,7 @@ pub struct Session { // Note: I don't think this one exists yet? Though I might've made a mistake and this might be a duplicate pub struct ClientInfo { pub client: Option, - pub os: String, + pub os: Option, pub version: u8, } diff --git a/src/types/events/user.rs b/src/types/events/user.rs index 877c96cd..fc72be4b 100644 --- a/src/types/events/user.rs +++ b/src/types/events/user.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::PublicUser; use crate::types::events::WebSocketEvent; use crate::types::utils::Snowflake; +use crate::types::Connection; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] /// See ; @@ -16,6 +17,27 @@ pub struct UserUpdate { pub user: PublicUser, } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] +/// Sent to indicate updates to a user's [Connection]. +/// +/// Not documented anywhere +pub struct UserConnectionsUpdate { + #[serde(flatten)] + pub connection: Connection, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] +/// See ; +/// +/// Sent when a note the current user has on another user is modified; +/// +/// If the field "note" is an empty string, the note was removed. +pub struct UserNoteUpdate { + /// Id of the user the note is for + pub id: Snowflake, + pub note: String, +} + #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, WebSocketEvent)] /// Undocumented; /// diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index 9e25093f..7a2543a6 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -4,10 +4,13 @@ use std::collections::HashMap; -use chrono::NaiveDate; +use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; -use crate::types::Snowflake; +use crate::types::{ + Connection, GuildAffinity, HarvestBackendType, Snowflake, ThemeColors, TwoWayLinkType, + UserAffinity, +}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] @@ -41,7 +44,12 @@ pub struct UserModifySchema { pub email: Option, /// The user's email token from their previous email, required if a new email is set. /// - /// See and + /// See: + /// + /// - the endpoints and + /// + /// - the relevant methods [`ChorusUser::initiate_email_change`](crate::instance::ChorusUser::initiate_email_change) and [`ChorusUser::verify_email_change`](crate::instance::ChorusUser::verify_email_change) + /// /// for changing the user's email. /// /// # Note @@ -111,3 +119,347 @@ pub struct PrivateChannelCreateSchema { pub access_tokens: Option>, pub nicks: Option>, } + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used to modify the current user's profile. +/// +/// Similar to [crate::types::UserProfileMetadata] +/// +/// See +pub struct UserModifyProfileSchema { + // Note: one of these causes a 500 if it is sent + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new pronouns (max 40 characters) + pub pronouns: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new bio (max 190 characters) + pub bio: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + // TODO: Add banner -- do we have an image data struct + /// The user's new accent color encoded as an i32 representation of a hex color code + pub accent_color: Option, + + // Note: without the skip serializing this currently (2024/07/28) causes a 500! + // + // Which in turns locks the user's account, requiring phone number verification + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new [ThemeColors] + pub theme_colors: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile popup animation particle type + pub popout_animation_particle_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile emoji id + pub emoji_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + /// The user's new profile ffect id + pub profile_effect_id: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used to delete or disable the current user's profile. +/// +/// See and +/// +pub struct DeleteDisableUserSchema { + /// The user's current password, if any + pub password: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// A schema used for [ChorusUser::verify_email_change](crate::instance::ChorusUser::verify_email_change) +/// +/// See +pub struct VerifyUserEmailChangeSchema { + /// The verification code sent to the user's email + pub code: String, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// The return type of [ChorusUser::verify_email_change](crate::instance::ChorusUser::verify_email_change) +/// +/// See +pub struct VerifyUserEmailChangeResponse { + /// The email_token to be used in [ChorusUser::modify](crate::instance::ChorusUser::modify) + #[serde(rename = "token")] + pub email_token: String, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] +/// Query string parameters for the route GET /users/{user.id}/profile +/// ([crate::types::User::get_profile]) +/// +/// See +pub struct GetUserProfileSchema { + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether to include the mutual guilds between the current user. + /// + /// If unset it will default to true + pub with_mutual_guilds: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether to include the mutual friends between the current user. + /// + /// If unset it will default to false + pub with_mutual_friends: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// Whether to include the number of mutual friends between the current user + /// + /// If unset it will default to false + pub with_mutual_friends_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The guild id to get the user's member profile in, if any. + /// + /// Note: + /// + /// when you click on a user in the member list in the discord client, a request is sent with + /// this property set to the selected guild id. + /// + /// This makes the request include fields such as guild_member and guild_member_profile + pub guild_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + /// The role id to get the user's application role connection metadata in, if any. + pub connections_role_id: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::get_pomelo_suggestions] endpoint. +/// +/// See +pub(crate) struct GetPomeloSuggestionsReturn { + pub username: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::get_pomelo_eligibility] endpoint. +/// +/// See +pub(crate) struct GetPomeloEligibilityReturn { + pub taken: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] +/// Query string parameters for the route GET /users/@me/mentions +/// ([crate::instance::ChorusUser::get_recent_mentions]) +/// +/// See +pub struct GetRecentMentionsSchema { + /// Only fetch messages before this message id + /// + /// Due to the nature of snowflakes, this can be easily used to fetch + /// messages before a certain timestamp + pub before: Option, + /// Max number of messages to return + /// + /// Should be between 1 and 100. + /// + /// If unset the limit is 25 messages + pub limit: Option, + /// Limit messages to a specific guild + pub guild_id: Option, + /// Whether to include role mentions. + /// + /// If unset the server assumes true + pub roles: Option, + /// Whether to include @everyone and @here mentions. + /// + /// If unset the server assumes true + pub everyone: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::create_harvest] endpoint. +// (koza): imo it's nicer if the user can just pass a vec, instead of having to bother with +// a specific type +/// +/// See +pub(crate) struct CreateUserHarvestSchema { + pub backends: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::set_user_note] endpoint. +/// +/// See +pub(crate) struct ModifyUserNoteSchema { + pub note: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Query string parameters for the route GET /connections/{connection.type}/authorize +/// ([crate::instance::ChorusUser::authorize_connection]) +/// +/// See +pub struct AuthorizeConnectionSchema { + /// The type of two-way link ([TwoWayLinkType]) to create + pub two_way_link_type: Option, + /// The device code to use for the two-way link + pub two_way_user_code: Option, + /// If this is a continuation of a previous authorization + pub continuation: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::authorize_connection] endpoint. +/// +/// See +pub(crate) struct AuthorizeConnectionReturn { + pub url: String, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route POST /connections/{connection.type}/callback ([crate::instance::ChorusUser::create_connection_callback]). +/// +/// See +pub struct CreateConnectionCallbackSchema { + /// The authorization code for the connection + pub code: String, + /// The "state" used to authorize a connection + // TODO: what is this? + pub state: String, + pub two_way_link_code: Option, + pub insecure: Option, + pub friend_sync: Option, + /// Additional parameters used for OpenID Connect + // FIXME: Is this correct? in other connections additional info + // is provided like this, only being string - string + pub openid_params: Option>, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route PUT /users/@me/connections/contacts/{connection.id} ([crate::instance::ChorusUser::create_contact_sync_connection]). +/// +/// See +pub struct CreateContactSyncConnectionSchema { + /// The username of the connection account + pub name: String, + /// Whether to sync friends over the connection + pub friend_sync: Option, +} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Json schema for the route PATCH /users/@me/connections/{connection.type}/{connection.id} ([crate::instance::ChorusUser::modify_connection]). +/// +/// Note: not all connection types support all parameters. +/// +/// See +pub struct ModifyConnectionSchema { + /// The connection account's username + /// + /// Note: We have not found which connection this could apply to + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// Whether activities related to this connection will be shown in presence + /// + /// e.g. on a Spotify connection, "Display Spotify as your status" + #[serde(skip_serializing_if = "Option::is_none")] + pub show_activity: Option, + + /// Whether or not to sync friends from this connection + /// + /// Note: we have not found which connections this can apply to + #[serde(skip_serializing_if = "Option::is_none")] + pub friend_sync: Option, + + /// Whether to show additional metadata on the user's profile + /// + /// e.g. on a Steam connection, "Display details on profile" + /// (number of games, items, member since) + /// + /// on a Twitter connection, number of posts / followers, member since + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_visibility: Option, + + /// Whether to show the connection on the user's profile + #[serde(skip_serializing_if = "Option::is_none")] + pub visibility: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +/// Internal type for the [crate::instance::ChorusUser::get_connection_access_token] endpoint. +/// +/// See +pub(crate) struct GetConnectionAccessTokenReturn { + pub access_token: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +/// Return type for the [crate::instance::ChorusUser::get_user_affinities] endpoint. +/// +/// See +pub struct UserAffinities { + pub user_affinities: Vec, + // FIXME: Is this also a UserAffinity vec? + // Also, no idea what this means + pub inverse_user_affinities: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +/// Return type for the [crate::instance::ChorusUser::get_guild_affinities] endpoint. +/// +/// See +pub struct GuildAffinities { + pub guild_affinities: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +/// Return type for the error in the [crate::instance::ChorusUser::create_domain_connection] endpoint. +/// +/// This allows us to retrieve the needed proof for actually verifying the connection. +/// +/// See +pub(crate) struct CreateDomainConnectionError { + pub message: String, + pub code: u16, + pub proof: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Return type for the [crate::instance::ChorusUser::create_domain_connection] endpoint. +/// +/// See +pub enum CreateDomainConnectionReturn { + /// Additional proof is needed to verify domain ownership. + /// + /// The inner object is a proof string (e.g. + /// `dh=dceaca792e3c40fcf356a9297949940af5cfe538`) + /// + /// To verify ownership, either: + /// + /// - add the proof string as a TXT DNS record to the domain, + /// with the name of the record being `_discord.{domain}` or + /// + /// - serve the proof string as a file at `https://{domain}/.well-known/discord` + /// + /// After either of these proofs are added, the request should be retried. + /// + ProofNeeded(String), + /// The domain connection was successfully created, no further action is needed. + /// + /// The inner object is the new connection. + Ok(Connection), +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +/// Return type for the [crate::instance::ChorusUser::get_burst_credits] endpoint. +/// +/// # Reference +/// ```json +/// { +/// "amount": 2, +/// "replenished_today": false, +/// "next_replenish_at": "2024-08-18T23:53:17+00:00" +/// } +/// ``` +pub struct BurstCreditsInfo { + /// Amount of remaining burst credits the local user has + pub amount: u16, + pub replenished_today: bool, + /// When the user's burst credits will automatically replenish again + pub next_replenish_at: DateTime, +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4b4f9c14..ac85b859 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use chorus::gateway::{Gateway, GatewayOptions}; -use chorus::types::{IntoShared, PermissionFlags}; +use chorus::types::{DeleteDisableUserSchema, IntoShared, PermissionFlags}; use chorus::{ instance::{ChorusUser, Instance}, types::{ @@ -146,5 +146,9 @@ pub(crate) async fn setup() -> TestBundle { pub(crate) async fn teardown(mut bundle: TestBundle) { let id = bundle.guild.read().unwrap().id; Guild::delete(&mut bundle.user, id).await.unwrap(); - bundle.user.delete().await.unwrap() + bundle + .user + .delete(DeleteDisableUserSchema { password: None }) + .await + .unwrap() } diff --git a/tests/user.rs b/tests/user.rs index 2fbc1874..e267ae36 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -2,7 +2,19 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use chorus::types::{PublicUser, Snowflake, User}; +use chorus::{ + errors::ChorusError, + types::{ + ConnectionType, DeleteDisableUserSchema, PublicUser, Snowflake, User, + UserModifyProfileSchema, UserNote, + }, +}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +mod common; #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] @@ -20,3 +32,181 @@ fn to_public_user() { let from_user = user.into_public_user(); assert_eq!(public_user, from_user); } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_get_user_profile() { + let mut bundle = common::setup().await; + + let user_id = bundle.user.object.read().unwrap().id; + + let user_profile = bundle + .user + .get_user_profile(user_id, chorus::types::GetUserProfileSchema::default()) + .await; + + assert!(user_profile.is_ok()); + + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_modify_user_profile() { + let mut bundle = common::setup().await; + + let bio = Some(String::from("A user.")); + let pronouns = Some(String::from("they/them")); + + let modify = UserModifyProfileSchema { + bio: bio.clone(), + pronouns: pronouns.clone(), + ..Default::default() + }; + + bundle.user.modify_profile(modify).await.unwrap(); + + let user_id = bundle.user.object.read().unwrap().id; + + let user_profile = bundle + .user + .get_user_profile(user_id, chorus::types::GetUserProfileSchema::default()) + .await + .unwrap(); + + assert_eq!(user_profile.profile_metadata.bio, bio); + assert_eq!(user_profile.profile_metadata.pronouns, pronouns.unwrap()); + + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_disable_user() { + let mut bundle = common::setup().await; + + let mut other_user = bundle.create_user("integrationtestuser4").await; + + other_user + .disable(DeleteDisableUserSchema { password: None }) + .await + .unwrap(); + + common::teardown(bundle).await; +} + +// Note: these two tests are currently broken. +// FIXME: readd them once bitfl0wer/server#2 is merged +/* +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_get_user_note() { + let mut bundle = common::setup().await; + + let mut other_user = bundle.create_user("integrationtestuser3").await; + + let user_id = bundle.user.object.read().unwrap().id; + let other_user_id = other_user.object.read().unwrap().id; + + let result = bundle.user.get_user_note(other_user_id).await; + assert!(matches!( + result.err().unwrap(), + ChorusError::NotFound { .. } + )); + + bundle + .user + .set_user_note(other_user_id, Some(String::from("A note."))) + .await + .unwrap(); + + assert!(false); + + let result = bundle.user.get_user_note(other_user_id).await; + assert_eq!( + result, + Ok(UserNote { + user_id, + note_user_id: other_user_id, + note: String::from("A note.") + }) + ); + + other_user + .delete(DeleteDisableUserSchema { password: None }) + .await + .unwrap(); + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_set_user_note() { + let mut bundle = common::setup().await; + + let mut other_user = bundle.create_user("integrationtestuser3").await; + + let user_id = bundle.user.object.read().unwrap().id; + let other_user_id = other_user.object.read().unwrap().id; + + bundle + .user + .set_user_note(other_user_id, Some(String::from("A note."))) + .await + .unwrap(); + + let result = bundle.user.get_user_note(other_user_id).await; + assert_eq!( + result, + Ok(UserNote { + user_id, + note_user_id: other_user_id, + note: String::from("A note.") + }) + ); + + other_user + .delete(DeleteDisableUserSchema { password: None }) + .await + .unwrap(); + common::teardown(bundle).await; +}*/ + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_get_user_affinities() { + let mut bundle = common::setup().await; + + let result = bundle.user.get_user_affinities().await.unwrap(); + + assert!(result.user_affinities.is_empty()); + assert!(result.inverse_user_affinities.is_empty()); + + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_get_guild_affinities() { + let mut bundle = common::setup().await; + + let result = bundle.user.get_guild_affinities().await.unwrap(); + + assert!(result.guild_affinities.is_empty()); + + common::teardown(bundle).await; +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn test_get_connections() { + let mut bundle = common::setup().await; + + let result = bundle.user.get_connections().await.unwrap(); + + // We can't *really* test creating or getting connections... + // TODO: Find a way? + assert!(result.is_empty()); + + common::teardown(bundle).await; +} From 08783cc5b8e1241392a994966ddd8a8112c4a70a Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 28 Sep 2024 10:32:33 +0200 Subject: [PATCH 11/23] Differentiate between instance softwares (#560) * Add InstanceOptions type * feat: add /ping and /version, implement software differentiation * fix: better error handling, missing slash in /policies/instance - handle deserialization errors and not parsing the http content into a string - the route only works on spacebar if you add a trailing slash for some reason --- src/api/instance.rs | 104 ++++++++++++++++++++++++++ src/api/mod.rs | 2 + src/api/policies/instance/instance.rs | 27 ++++++- src/gateway/options.rs | 19 +++++ src/instance.rs | 104 +++++++++++++++++++++++++- src/types/schema/instance.rs | 84 +++++++++++++++++++++ src/types/schema/mod.rs | 4 +- tests/instance.rs | 14 ++++ 8 files changed, 352 insertions(+), 6 deletions(-) create mode 100644 src/api/instance.rs create mode 100644 src/types/schema/instance.rs diff --git a/src/api/instance.rs b/src/api/instance.rs new file mode 100644 index 00000000..20680e41 --- /dev/null +++ b/src/api/instance.rs @@ -0,0 +1,104 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Contains miscellaneous api routes, such as /version and /ping +use serde_json::from_str; + +use crate::errors::{ChorusError, ChorusResult}; +use crate::instance::Instance; +use crate::types::{GeneralConfiguration, PingReturn, VersionReturn}; + +impl Instance { + /// Pings the instance, also fetches instance info. + /// + /// See: [PingReturn] + /// + /// # Notes + /// This is a Spacebar only endpoint. + /// + /// # Reference + /// See + pub async fn ping(&self) -> ChorusResult { + let endpoint_url = format!("{}/ping", self.urls.api.clone()); + + let request = match self.client.get(&endpoint_url).send().await { + Ok(result) => result, + Err(e) => { + return Err(ChorusError::RequestFailed { + url: endpoint_url, + error: e.to_string(), + }); + } + }; + + if !request.status().as_str().starts_with('2') { + return Err(ChorusError::ReceivedErrorCode { + error_code: request.status().as_u16(), + error: request.text().await.unwrap(), + }); + } + + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + match from_str::(&response_text) { + Ok(return_value) => Ok(return_value), + Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text) }) + } + } + + /// Fetches the instance's software implementation and version. + /// + /// See: [VersionReturn] + /// + /// # Notes + /// This is a Symfonia only endpoint. (For now, we hope that spacebar will adopt it as well) + pub async fn get_version(&self) -> ChorusResult { + let endpoint_url = format!("{}/version", self.urls.api.clone()); + + let request = match self.client.get(&endpoint_url).send().await { + Ok(result) => result, + Err(e) => { + return Err(ChorusError::RequestFailed { + url: endpoint_url, + error: e.to_string(), + }); + } + }; + + if !request.status().as_str().starts_with('2') { + return Err(ChorusError::ReceivedErrorCode { + error_code: request.status().as_u16(), + error: request.text().await.unwrap(), + }); + } + + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + match from_str::(&response_text) { + Ok(return_value) => Ok(return_value), + Err(e) => Err(ChorusError::InvalidResponse { error: format!("Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", e, response_text) }) + } + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index c9ca2792..5e2f7cab 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -10,6 +10,7 @@ pub use guilds::*; pub use invites::*; pub use policies::instance::instance::*; pub use users::*; +pub use instance::*; pub mod auth; pub mod channels; @@ -17,3 +18,4 @@ pub mod guilds; pub mod invites; pub mod policies; pub mod users; +pub mod instance; diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index 584db338..f2a40bc1 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -17,7 +17,7 @@ impl Instance { /// # Reference /// See pub async fn general_configuration_schema(&self) -> ChorusResult { - let endpoint_url = self.urls.api.clone() + "/policies/instance"; + let endpoint_url = self.urls.api.clone() + "/policies/instance/"; let request = match self.client.get(&endpoint_url).send().await { Ok(result) => result, Err(e) => { @@ -35,7 +35,28 @@ impl Instance { }); } - let body = request.text().await.unwrap(); - Ok(from_str::(&body).unwrap()) + let response_text = match request.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + + match from_str::(&response_text) { + Ok(object) => Ok(object), + Err(e) => { + Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + } } } diff --git a/src/gateway/options.rs b/src/gateway/options.rs index 4ff6178b..b8ded327 100644 --- a/src/gateway/options.rs +++ b/src/gateway/options.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use crate::instance::InstanceSoftware; + #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Debug, Default, Copy)] /// Options passed when initializing the gateway connection. /// @@ -22,6 +24,23 @@ pub struct GatewayOptions { } impl GatewayOptions { + /// Creates the ideal gateway options for an [InstanceSoftware], + /// based off which features it supports. + pub fn for_instance_software(software: InstanceSoftware) -> GatewayOptions { + // TODO: Support ETF + let encoding = GatewayEncoding::Json; + + let transport_compression = match software.supports_gateway_zlib() { + true => GatewayTransportCompression::ZLibStream, + false => GatewayTransportCompression::None, + }; + + GatewayOptions { + encoding, + transport_compression, + } + } + /// Adds the options to an existing gateway url /// /// Returns the new url diff --git a/src/instance.rs b/src/instance.rs index a8671e0a..f0569a99 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -28,11 +28,12 @@ use crate::UrlBundle; pub struct Instance { pub urls: UrlBundle, pub instance_info: GeneralConfiguration, + pub(crate) software: InstanceSoftware, pub limits_information: Option, #[serde(skip)] pub client: Client, #[serde(skip)] - pub gateway_options: GatewayOptions, + pub(crate) gateway_options: GatewayOptions, } #[derive(Debug, Clone, Serialize, Deserialize, Default, Eq)] @@ -72,6 +73,8 @@ impl Instance { /// If `options` is `None`, the default [`GatewayOptions`] will be used. /// /// To create an Instance from one singular url, use [`Instance::new()`]. + // Note: maybe make this just take urls and then add another method which creates an instance + // from urls and custom gateway options, since gateway options will be automatically generated? pub async fn from_url_bundle( urls: UrlBundle, options: Option, @@ -88,6 +91,7 @@ impl Instance { } else { limit_information = None } + let mut instance = Instance { urls: urls.clone(), // Will be overwritten in the next step @@ -95,7 +99,10 @@ impl Instance { limits_information: limit_information, client: Client::new(), gateway_options: options.unwrap_or_default(), + // Will also be detected soon + software: InstanceSoftware::Other, }; + instance.instance_info = match instance.general_configuration_schema().await { Ok(schema) => schema, Err(e) => { @@ -103,6 +110,13 @@ impl Instance { GeneralConfiguration::default() } }; + + instance.software = instance.detect_software().await; + + if options.is_none() { + instance.gateway_options = GatewayOptions::for_instance_software(instance.software()); + } + Ok(instance) } @@ -133,12 +147,98 @@ impl Instance { } } - /// Sets the [`GatewayOptions`] the instance will use when spawning new connections. + /// Detects which [InstanceSoftware] the instance is running. + pub async fn detect_software(&self) -> InstanceSoftware { + + if let Ok(version) = self.get_version().await { + match version.server.to_lowercase().as_str() { + "symfonia" => return InstanceSoftware::Symfonia, + // We can dream this will be implemented one day + "spacebar" => return InstanceSoftware::SpacebarTypescript, + _ => {} + } + } + + // We know it isn't a symfonia server now, work around spacebar + // not really having a version endpoint + let ping = self.ping().await; + + if ping.is_ok() { + return InstanceSoftware::SpacebarTypescript; + } + + InstanceSoftware::Other + } + + /// Returns the [`GatewayOptions`] the instance uses when spawning new connections. + /// + /// These options are used on the gateways created when logging in and registering. + pub fn gateway_options(&self) -> GatewayOptions { + self.gateway_options + } + + /// Manually sets the [`GatewayOptions`] the instance should use when spawning new connections. /// /// These options are used on the gateways created when logging in and registering. pub fn set_gateway_options(&mut self, options: GatewayOptions) { self.gateway_options = options; } + + /// Returns which [`InstanceSoftware`] the instance is running. + pub fn software(&self) -> InstanceSoftware { + self.software + } + + /// Manually sets which [`InstanceSoftware`] the instance is running. + /// + /// Note: you should only use this if you are absolutely sure about an instance (e. g. you run it). + /// If set to an incorrect value, this may cause unexpected errors or even undefined behaviours. + /// + /// Manually setting the software is generally discouraged. Chorus should automatically detect + /// which type of software the instance is running. + pub fn set_software(&mut self, software: InstanceSoftware) { + self.software = software; + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +/// The software implementation the spacebar-compatible instance is running. +/// +/// This is useful since some softwares may support additional features, +/// while other do not fully implement the api yet. +pub enum InstanceSoftware { + /// The official typescript Spacebar server, available + /// at + SpacebarTypescript, + /// The Polyphony server written in rust, available at + /// at + Symfonia, + /// We could not determine the instance software or it + /// is one we don't specifically differentiate. + /// + /// Assume it implements all features of the spacebar protocol. + #[default] + Other, +} + +impl InstanceSoftware { + /// Returns whether the software supports z-lib stream compression on the gateway + pub fn supports_gateway_zlib(self) -> bool { + match self { + InstanceSoftware::SpacebarTypescript => true, + InstanceSoftware::Symfonia => false, + InstanceSoftware::Other => true, + } + } + + /// Returns whether the software supports sending data in the Erlang external term format on the gateway + pub fn supports_gateway_etf(self) -> bool { + match self { + InstanceSoftware::SpacebarTypescript => true, + InstanceSoftware::Symfonia => false, + InstanceSoftware::Other => true, + } + } } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/types/schema/instance.rs b/src/types/schema/instance.rs new file mode 100644 index 00000000..7d19483c --- /dev/null +++ b/src/types/schema/instance.rs @@ -0,0 +1,84 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//! Contains schema for miscellaneous api routes, such as /version and /ping +//! +//! Implementations of those routes can be found in /api/instance.rs + +use serde::{Deserialize, Serialize}; + +use crate::types::{GeneralConfiguration, Snowflake}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +/// The return type of the spacebar-only /api/ping endpoint +pub struct PingReturn { + /// Note: always "pong!" + pub ping: String, + pub instance: PingInstance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(rename_all = "camelCase")] +/// [GeneralConfiguration] as returned from the /api/ping endpoint +pub struct PingInstance { + pub id: Option, + pub name: String, + pub description: Option, + pub image: Option, + pub correspondence_email: Option, + pub correspondence_user_id: Option, + pub front_page: Option, + pub tos_page: Option, +} + +impl PingInstance { + /// Converts self into the [GeneralConfiguration] type + pub fn into_general_configuration(self) -> GeneralConfiguration { + GeneralConfiguration { + instance_name: self.name, + instance_description: self.description, + front_page: self.front_page, + tos_page: self.tos_page, + correspondence_email: self.correspondence_email, + correspondence_user_id: self.correspondence_user_id, + image: self.image, + instance_id: self.id, + } + } + + /// Converts the [GeneralConfiguration] type into self + pub fn from_general_configuration(other: GeneralConfiguration) -> Self { + Self { + id: other.instance_id, + name: other.instance_name, + description: other.instance_description, + image: other.image, + correspondence_email: other.correspondence_email, + correspondence_user_id: other.correspondence_user_id, + front_page: other.front_page, + tos_page: other.tos_page, + } + } +} + +impl From for GeneralConfiguration { + fn from(value: PingInstance) -> Self { + value.into_general_configuration() + } +} + +impl From for PingInstance { + fn from(value: GeneralConfiguration) -> Self { + Self::from_general_configuration(value) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +/// The return type of the symfonia-only /version endpoint +pub struct VersionReturn { + /// The instance's software version, e. g. "0.1.0" + pub version: String, + /// The instance's software, e. g. "symfonia" or "spacebar" + pub server: String, +} diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs index 09e542e4..2888046e 100644 --- a/src/types/schema/mod.rs +++ b/src/types/schema/mod.rs @@ -13,6 +13,7 @@ pub use role::*; pub use user::*; pub use invites::*; pub use voice_state::*; +pub use instance::*; mod apierror; mod audit_log; @@ -25,9 +26,10 @@ mod role; mod user; mod invites; mod voice_state; +mod instance; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct GenericSearchQueryWithLimit { pub query: String, pub limit: Option, -} \ No newline at end of file +} diff --git a/tests/instance.rs b/tests/instance.rs index eb5fc606..d83e5e74 100644 --- a/tests/instance.rs +++ b/tests/instance.rs @@ -3,6 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. mod common; +use chorus::instance::InstanceSoftware; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] @@ -19,3 +20,16 @@ async fn generate_general_configuration_schema() { .unwrap(); common::teardown(bundle).await; } + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +async fn detect_instance_software() { + let bundle = common::setup().await; + + let software = bundle.instance.detect_software().await; + assert_eq!(software, InstanceSoftware::SpacebarTypescript); + + assert_eq!(bundle.instance.software(), InstanceSoftware::SpacebarTypescript); + + common::teardown(bundle).await; +} From 3aac8ea4e6917c5535d3cc31c5a1656554494c28 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Sat, 28 Sep 2024 23:09:49 +0200 Subject: [PATCH 12/23] Ready Event Updates (#561) * Update Ready, Split Ready into User and Bot variants * add supplemental to_bot method for gatewayready * Use serde(default) to fix missing attrs in spacebar server impl --- src/types/events/ready.rs | 107 +++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 12 deletions(-) diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index ffba526a..b5aceea1 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -2,38 +2,121 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use crate::types::entities::{Guild, User}; use crate::types::events::{Session, WebSocketEvent}; -use crate::types::{Activity, Channel, ClientStatusObject, GuildMember, PresenceUpdate, Snowflake, VoiceState}; +use crate::types::{ + Activity, Channel, ClientStatusObject, GuildMember, PresenceUpdate, Relationship, Snowflake, + UserSettings, VoiceState, +}; #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] -/// 1/2 officially documented; -/// Received after identifying, provides initial user info; +/// Received after identifying, provides initial user info /// /// See and -// TODO: There are a LOT of fields missing here pub struct GatewayReady { - pub analytics_token: Option, - pub auth_session_id_hash: Option, - pub country_code: Option, + pub analytics_token: String, + pub auth_session_id_hash: String, + pub country_code: String, + pub v: u8, + pub user: User, + #[serde(default)] + pub guilds: Vec, + pub presences: Option>, + pub sessions: Option>, + pub session_id: String, + pub session_type: String, + pub resume_gateway_url: String, + pub shard: Option<(u64, u64)>, + pub user_settings: Option, + pub user_settings_proto: Option, + #[serde(default)] + pub relationships: Vec, + pub friend_suggestion_count: u32, + #[serde(default)] + pub private_channels: Vec, + #[serde(default)] + pub notes: HashMap, + pub merged_presences: Option, + #[serde(default)] + pub users: Vec, + pub auth_token: Option, + #[serde(default)] + pub authenticator_types: Vec, + pub required_action: Option, + #[serde(default)] + pub geo_ordered_rtc_regions: Vec, + /// TODO: Make tutorial object into object + pub tutorial: Option, + pub api_code_version: u8, + #[serde(default)] + pub experiments: Vec, + #[serde(default)] + pub guild_experiments: Vec, +} +#[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] +/// Received after identifying, provides initial information about the bot session. +/// +/// See and +pub struct GatewayReadyBot { pub v: u8, pub user: User, - /// For bots these are [crate::types::UnavailableGuild]s, for users they are [Guild] pub guilds: Vec, pub presences: Option>, pub sessions: Option>, pub session_id: String, - pub session_type: Option, - pub resume_gateway_url: Option, + pub session_type: String, + pub resume_gateway_url: String, pub shard: Option<(u64, u64)>, + pub merged_presences: Option, + pub users: Vec, + pub authenticator_types: Vec, + pub geo_ordered_rtc_regions: Vec, + pub api_code_version: u8, +} + +impl From for GatewayReadyBot { + fn from(value: GatewayReady) -> Self { + GatewayReadyBot { + v: value.v, + user: value.user, + guilds: value.guilds, + presences: value.presences, + sessions: value.sessions, + session_id: value.session_id, + session_type: value.session_type, + resume_gateway_url: value.resume_gateway_url, + shard: value.shard, + merged_presences: value.merged_presences, + users: value.users, + authenticator_types: value.authenticator_types, + geo_ordered_rtc_regions: value.geo_ordered_rtc_regions, + api_code_version: value.api_code_version, + } + } +} + +impl GatewayReady { + /// Convert this struct into a [GatewayReadyBot] struct + pub fn to_bot(self) -> GatewayReadyBot { + self.into() + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] +#[repr(u8)] +pub enum AuthenticatorType { + WebAuthn = 1, + Totp = 2, + Sms = 3, } #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] /// Officially Undocumented; -/// Sent after the READY event when a client is a user, +/// Sent after the READY event when a client is a user, /// seems to somehow add onto the ready event; /// /// See @@ -52,7 +135,7 @@ pub struct GatewayReadySupplemental { pub struct MergedPresences { /// "Presences of the user's guilds in the same order as the guilds array in ready" /// (discord.sex) - pub guilds: Vec>, + pub guilds: Vec>, /// "Presences of the user's friends and implicit relationships" (discord.sex) pub friends: Vec, } From 048d129f521101c644468752f39d91b81aaf37ef Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 29 Sep 2024 12:04:25 +0200 Subject: [PATCH 13/23] Add "Merging" section --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84e70210..0bad470e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,3 +10,7 @@ ever since [we streamlined the process of doing so](https://github.com/polyphony If you'd like to contribute new functionality, check out [The 'Meta'-issues.](https://github.com/polyphony-chat/chorus/issues?q=is%3Aissue+label%3A%22Type%3A+Meta%22+) They contain a comprehensive list of all features which are yet missing for full Discord.com compatibility. Please feel free to open an Issue with the idea you have, or a Pull Request. + +## Merging + +All pull requests opened into the `dev` branch should be merged via the "Squash and Merge" option to keep the commit history small. Merging into the `main` branch should be done via a regular merge commit. This way, GitHub will correctly attribute contributors and count statistics for the insights tab. From 36a4ac9b516aaa18d33dc056759f8903605db343 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 29 Sep 2024 13:26:18 +0200 Subject: [PATCH 14/23] Document all fields of GatewayReady and GatewayReadyBot --- src/types/events/ready.rs | 65 +++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index b5aceea1..98bdaa6c 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -14,47 +14,80 @@ use crate::types::{ }; #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] -/// Received after identifying, provides initial user info +/// Received after identifying, provides initial user information and client state. /// /// See and pub struct GatewayReady { + #[serde(default)] + /// An array of stringified JSON values representing the connection trace, used for debugging + pub _trace: Vec, + /// The token used for analytical tracking requests pub analytics_token: String, + /// The hash of the auth session ID corresponding to the auth token used to connect pub auth_session_id_hash: String, + /// The detected ISO 3166-1 alpha-2 country code of the user's current IP address pub country_code: String, - pub v: u8, + #[serde(rename = "v")] + /// API version + pub api_version: u8, + /// The connected user pub user: User, #[serde(default)] + /// The guilds the user is in pub guilds: Vec, + /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability). pub presences: Option>, pub sessions: Option>, + /// Unique session ID, used for resuming connections pub session_id: String, + /// The type of session that was started pub session_type: String, + /// WebSocket URL for resuming connections pub resume_gateway_url: String, + /// The shard information (shard_id, num_shards) associated with this session, if sharded pub shard: Option<(u64, u64)>, + /// The client settings for the user pub user_settings: Option, + /// The base-64 encoded preloaded user settings for the user, (if missing, defaults are used) pub user_settings_proto: Option, #[serde(default)] + /// The relationships the user has with other users pub relationships: Vec, + /// The number of friend suggestions the user has pub friend_suggestion_count: u32, #[serde(default)] + /// The DMs and group DMs the user is participating in pub private_channels: Vec, #[serde(default)] + /// A mapping of user IDs to notes the user has made for them pub notes: HashMap, + /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability), and any guild presences sent at startup pub merged_presences: Option, #[serde(default)] + /// The deduped users across all objects in the event pub users: Vec, + /// The refreshed auth token for this user; if present, the client should discard the current auth token and use this in subsequent requests to the API pub auth_token: Option, #[serde(default)] + /// The types of multi-factor authenticators the user has enabled pub authenticator_types: Vec, + /// The action a user is required to take before continuing to use Discord pub required_action: Option, #[serde(default)] + /// A geo-ordered list of RTC regions that can be used when when setting a voice channel's `rtc_region` or updating the client's voice state pub geo_ordered_rtc_regions: Vec, + /// The tutorial state of the user, if any /// TODO: Make tutorial object into object pub tutorial: Option, + /// The API code version, used when re-identifying with client state v2 pub api_code_version: u8, #[serde(default)] + /// User experiment rollouts for the user + /// TODO: Make User Experiments into own struct pub experiments: Vec, #[serde(default)] + /// Guild experiment rollouts for the user + /// TODO: Make Guild Experiments into own struct pub guild_experiments: Vec, } @@ -63,30 +96,49 @@ pub struct GatewayReady { /// /// See and pub struct GatewayReadyBot { - pub v: u8, + #[serde(default)] + /// An array of stringified JSON values representing the connection trace, used for debugging + pub _trace: Vec, + #[serde(rename = "v")] + /// API version + pub api_version: u8, + /// The connected bot user pub user: User, + #[serde(default)] + /// The guilds the bot user is in. Will be `UnavailableGuilds` at first. pub guilds: Vec, + /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability). pub presences: Option>, - pub sessions: Option>, + /// Unique session ID, used for resuming connections pub session_id: String, + /// The type of session that was started pub session_type: String, + /// WebSocket URL for resuming connections pub resume_gateway_url: String, + /// The shard information (shard_id, num_shards) associated with this session, if sharded pub shard: Option<(u64, u64)>, + /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability), and any guild presences sent at startup pub merged_presences: Option, + #[serde(default)] + /// The deduped users across all objects in the event pub users: Vec, + #[serde(default)] + /// The types of multi-factor authenticators the user has enabled pub authenticator_types: Vec, + #[serde(default)] + /// A geo-ordered list of RTC regions that can be used when when setting a voice channel's `rtc_region` or updating the client's voice state pub geo_ordered_rtc_regions: Vec, + /// The API code version, used when re-identifying with client state v2 pub api_code_version: u8, } impl From for GatewayReadyBot { fn from(value: GatewayReady) -> Self { GatewayReadyBot { - v: value.v, + api_version: value.api_version, user: value.user, guilds: value.guilds, presences: value.presences, - sessions: value.sessions, session_id: value.session_id, session_type: value.session_type, resume_gateway_url: value.resume_gateway_url, @@ -96,6 +148,7 @@ impl From for GatewayReadyBot { authenticator_types: value.authenticator_types, geo_ordered_rtc_regions: value.geo_ordered_rtc_regions, api_code_version: value.api_code_version, + _trace: value._trace, } } } From e0c6fce4b59f748b83e5a16ee95119abb2227e6b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 29 Sep 2024 15:13:35 +0200 Subject: [PATCH 15/23] Fix PostgreSQL compatibility of READY and some newer enum types --- src/types/entities/user.rs | 23 ++++++++++++----------- src/types/events/ready.rs | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index 9c7c3b9d..bd74a8cb 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -7,7 +7,7 @@ use crate::types::utils::Snowflake; use crate::{UInt32, UInt8}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use serde_aux::prelude::{deserialize_option_number_from_string, deserialize_default_from_null}; +use serde_aux::prelude::{deserialize_default_from_null, deserialize_option_number_from_string}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::array::TryFromSliceError; use std::fmt::Debug; @@ -234,7 +234,8 @@ bitflags::bitflags! { Ord, )] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] -#[repr(u8)] +#[cfg_attr(not(feature = "sqlx"), repr(u8))] +#[cfg_attr(feature = "sqlx", repr(i16))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] /// **User** premium (Nitro) type /// @@ -258,9 +259,9 @@ pub struct UserProfileMetadata { /// The guild ID this profile applies to, if it is a guild profile. pub guild_id: Option, /// The user's pronouns, up to 40 characters - #[serde(deserialize_with = "deserialize_default_from_null")] - // Note: spacebar will send this is as null, while it should be "" - // See issue 1188 + #[serde(deserialize_with = "deserialize_default_from_null")] + // Note: spacebar will send this is as null, while it should be "" + // See issue 1188 pub pronouns: String, /// The user's bio / description, up to 190 characters pub bio: Option, @@ -847,13 +848,13 @@ pub struct PremiumUsageData { } impl From for usize { - fn from(value: PremiumUsageData) -> Self { - value.value - } + fn from(value: PremiumUsageData) -> Self { + value.value + } } impl From for PremiumUsageData { - fn from(value: usize) -> Self { - PremiumUsageData { value } - } + fn from(value: usize) -> Self { + PremiumUsageData { value } + } } diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index 98bdaa6c..d8c11de1 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -12,6 +12,7 @@ use crate::types::{ Activity, Channel, ClientStatusObject, GuildMember, PresenceUpdate, Relationship, Snowflake, UserSettings, VoiceState, }; +use crate::{UInt32, UInt64, UInt8}; #[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)] /// Received after identifying, provides initial user information and client state. @@ -29,7 +30,7 @@ pub struct GatewayReady { pub country_code: String, #[serde(rename = "v")] /// API version - pub api_version: u8, + pub api_version: UInt8, /// The connected user pub user: User, #[serde(default)] @@ -45,7 +46,7 @@ pub struct GatewayReady { /// WebSocket URL for resuming connections pub resume_gateway_url: String, /// The shard information (shard_id, num_shards) associated with this session, if sharded - pub shard: Option<(u64, u64)>, + pub shard: Option<(UInt64, UInt64)>, /// The client settings for the user pub user_settings: Option, /// The base-64 encoded preloaded user settings for the user, (if missing, defaults are used) @@ -54,7 +55,7 @@ pub struct GatewayReady { /// The relationships the user has with other users pub relationships: Vec, /// The number of friend suggestions the user has - pub friend_suggestion_count: u32, + pub friend_suggestion_count: UInt32, #[serde(default)] /// The DMs and group DMs the user is participating in pub private_channels: Vec, @@ -80,7 +81,7 @@ pub struct GatewayReady { /// TODO: Make tutorial object into object pub tutorial: Option, /// The API code version, used when re-identifying with client state v2 - pub api_code_version: u8, + pub api_code_version: UInt8, #[serde(default)] /// User experiment rollouts for the user /// TODO: Make User Experiments into own struct @@ -101,7 +102,7 @@ pub struct GatewayReadyBot { pub _trace: Vec, #[serde(rename = "v")] /// API version - pub api_version: u8, + pub api_version: UInt8, /// The connected bot user pub user: User, #[serde(default)] @@ -116,7 +117,7 @@ pub struct GatewayReadyBot { /// WebSocket URL for resuming connections pub resume_gateway_url: String, /// The shard information (shard_id, num_shards) associated with this session, if sharded - pub shard: Option<(u64, u64)>, + pub shard: Option<(UInt64, UInt64)>, /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability), and any guild presences sent at startup pub merged_presences: Option, #[serde(default)] @@ -129,7 +130,7 @@ pub struct GatewayReadyBot { /// A geo-ordered list of RTC regions that can be used when when setting a voice channel's `rtc_region` or updating the client's voice state pub geo_ordered_rtc_regions: Vec, /// The API code version, used when re-identifying with client state v2 - pub api_code_version: u8, + pub api_code_version: UInt8, } impl From for GatewayReadyBot { @@ -160,7 +161,8 @@ impl GatewayReady { } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)] -#[repr(u8)] +#[cfg_attr(not(feature = "sqlx"), repr(u8))] +#[cfg_attr(feature = "sqlx", repr(i16))] pub enum AuthenticatorType { WebAuthn = 1, Totp = 2, From 36073f1e24a1158520454acbfdb6e0e1a400e29b Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Mon, 30 Sep 2024 23:12:47 +0200 Subject: [PATCH 16/23] Adjust premium_type field to be of type PremiumType (#563) --- src/types/config/types/subconfigs/defaults/user.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/config/types/subconfigs/defaults/user.rs b/src/types/config/types/subconfigs/defaults/user.rs index a533b0ac..982507ea 100644 --- a/src/types/config/types/subconfigs/defaults/user.rs +++ b/src/types/config/types/subconfigs/defaults/user.rs @@ -4,11 +4,13 @@ use serde::{Deserialize, Serialize}; +use crate::types::PremiumType; + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy, Hash, PartialOrd, Ord)] #[serde(rename_all = "camelCase")] pub struct UserDefaults { pub premium: bool, - pub premium_type: u8, + pub premium_type: PremiumType, pub verified: bool, } @@ -16,7 +18,7 @@ impl Default for UserDefaults { fn default() -> Self { Self { premium: true, - premium_type: 2, + premium_type: PremiumType::Tier2, verified: true, } } From 9db77fb515c7a18770a9ce7037ec30ce84541278 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:38:33 +0200 Subject: [PATCH 17/23] Implement custom sqlx::Postgres en-/decoding for PremiumType enum (#565) --- src/types/entities/user.rs | 48 +++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index bd74a8cb..5e761307 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -233,9 +233,7 @@ bitflags::bitflags! { PartialOrd, Ord, )] -#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] -#[cfg_attr(not(feature = "sqlx"), repr(u8))] -#[cfg_attr(feature = "sqlx", repr(i16))] +#[repr(u8)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] /// **User** premium (Nitro) type /// @@ -252,6 +250,50 @@ pub enum PremiumType { Tier3 = 3, } +impl TryFrom for PremiumType { + type Error = ChorusError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Tier1), + 2 => Ok(Self::Tier2), + 3 => Ok(Self::Tier3), + _ => Err(ChorusError::InvalidArguments { + error: "Value is not a valid PremiumType".to_string(), + }), + } + } +} + +#[cfg(feature = "sqlx")] +impl sqlx::Type for PremiumType { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for PremiumType { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::from(*self as u8); + sqlx_pg_uint.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for PremiumType { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::decode(value)?; + PremiumType::try_from(sqlx_pg_uint.to_uint()).map_err(|e| e.into()) + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] /// # Reference /// See From 7db34f72a2f6bb6b440040d69e4fcf1c60b519c5 Mon Sep 17 00:00:00 2001 From: Flori <39242991+bitfl0wer@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:37:35 +0200 Subject: [PATCH 18/23] Make Relationship struct sqlx/symfonia-ready (#567) --- src/types/entities/relationship.rs | 60 +++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index 08cb41f1..e6d14f42 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -6,18 +6,28 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::errors::ChorusError; use crate::types::{Shared, Snowflake}; use super::{arc_rwlock_ptr_eq, PublicUser}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] /// See pub struct Relationship { + /// The ID of the target user + #[cfg_attr(feature = "sqlx", sqlx(rename = "to_id"))] pub id: Snowflake, #[serde(rename = "type")] + #[cfg_attr(feature = "sqlx", sqlx(rename = "type"))] pub relationship_type: RelationshipType, + #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id + /// The nickname of the user in this relationship pub nickname: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id + /// The target user pub user: Shared, + /// When the user requested a relationship pub since: Option>, } @@ -45,8 +55,7 @@ impl PartialEq for Relationship { Copy, Hash, )] -#[cfg_attr(not(feature = "sqlx"), repr(u8))] -#[cfg_attr(feature = "sqlx", repr(i16))] +#[repr(u8)] /// See pub enum RelationshipType { Suggestion = 6, @@ -58,3 +67,50 @@ pub enum RelationshipType { Friends = 1, None = 0, } + +#[cfg(feature = "sqlx")] +impl sqlx::Type for RelationshipType { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +#[cfg(feature = "sqlx")] +impl<'q> sqlx::Encode<'q, sqlx::Postgres> for RelationshipType { + fn encode_by_ref( + &self, + buf: &mut ::ArgumentBuffer<'q>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::from(*self as u8); + sqlx_pg_uint.encode_by_ref(buf) + } +} + +#[cfg(feature = "sqlx")] +impl<'r> sqlx::Decode<'r, sqlx::Postgres> for RelationshipType { + fn decode( + value: ::ValueRef<'r>, + ) -> Result { + let sqlx_pg_uint = sqlx_pg_uint::PgU8::decode(value)?; + Self::try_from(sqlx_pg_uint.to_uint()).map_err(|e| e.into()) + } +} + +impl TryFrom for RelationshipType { + type Error = ChorusError; + + fn try_from(value: u8) -> Result { + match value { + 6 => Ok(Self::Suggestion), + 5 => Ok(Self::Implicit), + 4 => Ok(Self::Outgoing), + 3 => Ok(Self::Incoming), + 2 => Ok(Self::Blocked), + 1 => Ok(Self::Friends), + 0 => Ok(Self::None), + _ => Err(ChorusError::InvalidArguments { + error: format!("Value {} is not a valid RelationshipType", value), + }), + } + } +} From 720531d93f9b2a41d255b4ad5b88734f5e45378b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 8 Oct 2024 17:18:39 +0200 Subject: [PATCH 19/23] #567: Append: Nickname *cannot* be derived from user id --- src/types/entities/relationship.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/entities/relationship.rs b/src/types/entities/relationship.rs index e6d14f42..da2a9dc7 100644 --- a/src/types/entities/relationship.rs +++ b/src/types/entities/relationship.rs @@ -21,7 +21,6 @@ pub struct Relationship { #[serde(rename = "type")] #[cfg_attr(feature = "sqlx", sqlx(rename = "type"))] pub relationship_type: RelationshipType, - #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id /// The nickname of the user in this relationship pub nickname: Option, #[cfg_attr(feature = "sqlx", sqlx(skip))] // Can be derived from the user id From dd4e7ae731bf9e89ecadafdf415095d4f4a94f46 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 10 Oct 2024 19:00:01 +0200 Subject: [PATCH 20/23] i dont wanna talk about it --- Cargo.lock | 306 +++++++++++++++------------------- Cargo.toml | 4 +- src/api/auth/login.rs | 10 -- src/api/auth/mod.rs | 6 - src/api/auth/register.rs | 13 +- src/api/users/users.rs | 8 - src/instance.rs | 3 +- src/types/entities/message.rs | 2 - src/types/schema/channel.rs | 23 --- src/types/schema/role.rs | 1 - src/types/utils/serde.rs | 12 +- 11 files changed, 147 insertions(+), 241 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f36d5a4f..067322f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,13 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -77,13 +71,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -114,23 +108,23 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -209,15 +203,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.14" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" dependencies = [ "shlex", ] @@ -276,7 +270,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "wasmtimer", - "webpki-roots 0.26.3", + "webpki-roots 0.26.6", "ws_stream_wasm", ] @@ -286,7 +280,7 @@ version = "0.5.0" dependencies = [ "async-trait", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -358,9 +352,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -457,7 +451,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -468,7 +462,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -590,19 +584,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "flate2" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" -dependencies = [ - "crc32fast", - "miniz_oxide 0.8.0", -] - -[[package]] -name = "flate2" -version = "1.0.30" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -701,7 +685,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -760,9 +744,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "h2" @@ -776,7 +760,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.4.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -795,7 +779,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.4.0", + "indexmap 2.5.0", "slab", "tokio", "tokio-util", @@ -1035,9 +1019,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-util", @@ -1050,9 +1034,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1100,9 +1084,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -1120,9 +1104,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "ipnetwork" @@ -1173,9 +1157,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -1270,15 +1254,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1411,9 +1386,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -1432,9 +1407,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -1454,7 +1429,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -1534,9 +1509,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "pnet_base" @@ -1570,9 +1545,9 @@ dependencies = [ [[package]] name = "poem" -version = "3.0.4" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1ba1c27f8f89e1bccdda0c680f72790545a11a8d8555819472f5839d7a8ca9d" +checksum = "e5419c612a492fce4961c521dca0c2249b5c48dc46eb5c8048063843f37a711d" dependencies = [ "bytes", "futures-util", @@ -1604,14 +1579,14 @@ dependencies = [ [[package]] name = "poem-derive" -version = "3.0.4" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62fea1692d80a000126f9b28d865012a160b80000abb53ccf152b428222c155" +checksum = "cdfed15c1102d2a9a51b9f1aba945628c72ccb52fc5d3e4ad4ffbbd222e11821" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -1642,9 +1617,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -1708,18 +1683,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags 2.6.0", ] @@ -1862,18 +1828,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -1896,14 +1862,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.12" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.6", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] @@ -1929,9 +1895,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -1945,9 +1911,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -2014,9 +1980,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] @@ -2034,20 +2000,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] name = "serde_json" -version = "1.0.127" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", "memchr", @@ -2063,7 +2029,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -2088,7 +2054,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.4.0", + "indexmap 2.5.0", "serde", "serde_derive", "serde_json", @@ -2105,7 +2071,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -2223,9 +2189,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -2233,9 +2199,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcfa89bea9500db4a0d038513d7a060566bfc51d46d1c014847049a45cce85e8" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2246,9 +2212,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06e2f2bd861719b1f3f0c7dbe1d80c30bf59e76cf019f07d9014ed7eefb8e08" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" dependencies = [ "atoi", "bigdecimal", @@ -2267,14 +2233,14 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.4.0", + "indexmap 2.5.0", "ipnetwork", "log", "memchr", "once_cell", "paste", "percent-encoding", - "rustls 0.23.12", + "rustls 0.23.13", "rustls-pemfile 2.1.3", "serde", "serde_json", @@ -2286,27 +2252,27 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots 0.26.3", + "webpki-roots 0.26.6", ] [[package]] name = "sqlx-macros" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f998a9defdbd48ed005a89362bd40dd2117502f15294f61c8d47034107dbbdc" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] name = "sqlx-macros-core" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d100558134176a2629d46cec0c8891ba0be8910f7896abfdb75ef4ab6f4e7ce" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" dependencies = [ "dotenvy", "either", @@ -2322,7 +2288,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.76", + "syn 2.0.79", "tempfile", "tokio", "url", @@ -2330,9 +2296,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cac0ab331b14cb3921c62156d913e4c15b74fb6ec0f3146bd4ef6e4fb3c12" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", "base64 0.22.1", @@ -2374,9 +2340,9 @@ dependencies = [ [[package]] name = "sqlx-pg-uint" -version = "0.5.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1cfe6c40c1cd0053b9029a41729a533ceb32093052df626aa8bfbba45e45f6" +checksum = "60e5ec2fd2d274ebf9ad6b44b3986f9bcdbb554bb162c4b1ac4af05a439c66f2" dependencies = [ "bigdecimal", "serde", @@ -2387,19 +2353,19 @@ dependencies = [ [[package]] name = "sqlx-pg-uint-macros" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae3447aced07f8bc71d73dc8dd1c6d25c2f4d10ea62a22ceabc12af8410d7e2" +checksum = "0e527060e9f43479e5b386e4237ab320a36fce39394f6ed73c8870f4637f2e5f" dependencies = [ "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] name = "sqlx-postgres" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9734dbce698c67ecf67c442f768a5e90a49b2a4d61a9f1d59f73874bd4cf0710" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", "base64 0.22.1", @@ -2439,9 +2405,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75b419c3c1b1697833dd927bdc4c6545a620bc1bbafabd44e1efbe9afcd337e" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" dependencies = [ "atoi", "chrono", @@ -2497,9 +2463,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.76" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -2544,9 +2510,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -2557,22 +2523,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -2623,9 +2589,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", @@ -2645,7 +2611,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -2660,9 +2626,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -2686,9 +2652,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -2705,11 +2671,11 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.4.0", + "indexmap 2.5.0", "toml_datetime", "winnow", ] @@ -2740,7 +2706,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -2810,15 +2776,15 @@ checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] @@ -2939,7 +2905,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", "wasm-bindgen-shared", ] @@ -2973,7 +2939,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3007,7 +2973,7 @@ checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] @@ -3042,20 +3008,20 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall", "wasite", ] @@ -3255,9 +3221,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -3309,7 +3275,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.79", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6d4830ed..031aeff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ jsonwebtoken = "8.3.0" log = "0.4.22" async-trait = "0.1.81" chorus-macros = { path = "./chorus-macros", version = "0" } # Note: version here is used when releasing. This will use the latest release. Make sure to republish the crate when code in macros is changed! -sqlx = { version = "0.8.1", features = [ +sqlx = { version = "0.8.2", features = [ "json", "chrono", "ipnetwork", @@ -67,7 +67,7 @@ rand = "0.8.5" flate2 = { version = "1.0.33", optional = true } webpki-roots = "0.26.3" pubserve = { version = "1.1.0", features = ["async", "send"] } -sqlx-pg-uint = { version = "0.5.0", features = ["serde"], optional = true } +sqlx-pg-uint = { version = "0.7.2", features = ["serde"], optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = "0.21.12" diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 4d8cbce6..ab78a995 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -30,22 +30,12 @@ impl Instance { // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since login is an instance wide limit), which is why we are just cloning the // instances' limits to pass them on as user_rate_limits later. -<<<<<<< HEAD let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None").await; let login_result = chorus_request .deserialize_response::(&mut user) .await?; user.set_token(&login_result.token); -======= - let mut user = - ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; - - let login_result = chorus_request - .deserialize_response::(&mut user) - .await?; - user.set_token(login_result.token); ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) user.settings = login_result.settings; let object = User::get_current(&mut user).await?; diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs index cdc41ba8..96491351 100644 --- a/src/api/auth/mod.rs +++ b/src/api/auth/mod.rs @@ -22,14 +22,8 @@ pub mod register; impl Instance { /// Logs into an existing account on the spacebar server, using only a token. -<<<<<<< HEAD pub async fn login_with_token(&mut self, token: &str) -> ChorusResult { let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; -======= - pub async fn login_with_token(&mut self, token: String) -> ChorusResult { - let mut user = - ChorusUser::shell(Arc::new(RwLock::new(self.clone())), token).await; ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) let object = User::get_current(&mut user).await?; let settings = User::get_settings(&mut user).await?; diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index 532a65c6..d978e0ff 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -37,25 +37,16 @@ impl Instance { // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since register is an instance wide limit), which is why we are just cloning // the instances' limits to pass them on as user_rate_limits later. -<<<<<<< HEAD let mut user = ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None").await; -======= - let mut user = - ChorusUser::shell(Arc::new(RwLock::new(self.clone())), "None".to_string()).await; - ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) let token = chorus_request .deserialize_response::(&mut user) .await? .token; -<<<<<<< HEAD + user.set_token(&token); -======= - user.set_token(token); ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) - let object = User::get(&mut user, None).await?; + let object = User::get_current(&mut user).await?; let settings = User::get_settings(&mut user).await?; *user.object.write().unwrap() = object; diff --git a/src/api/users/users.rs b/src/api/users/users.rs index b8ab8d6a..483a85c1 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -82,11 +82,7 @@ impl ChorusUser { /// # Notes /// This function is a wrapper around [`User::get_settings`]. pub async fn get_settings(&mut self) -> ChorusResult { -<<<<<<< HEAD User::get_settings(self).await -======= - User::get_settings(self).await ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) } /// Modifies the current user's representation. (See [`User`]) @@ -882,7 +878,3 @@ impl User { chorus_request.handle_request_as_result(user).await } } -<<<<<<< HEAD -======= - ->>>>>>> 03f1e7d (Refactor / fix login and register (#495)) diff --git a/src/instance.rs b/src/instance.rs index f0569a99..3956dd06 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -149,8 +149,7 @@ impl Instance { /// Detects which [InstanceSoftware] the instance is running. pub async fn detect_software(&self) -> InstanceSoftware { - - if let Ok(version) = self.get_version().await { + if let Ok(version) = self.get_version().await { match version.server.to_lowercase().as_str() { "symfonia" => return InstanceSoftware::Symfonia, // We can dream this will be implemented one day diff --git a/src/types/entities/message.rs b/src/types/entities/message.rs index ef7f392e..b63eb3bf 100644 --- a/src/types/entities/message.rs +++ b/src/types/entities/message.rs @@ -19,8 +19,6 @@ use crate::{UInt32, UInt8}; use super::option_arc_rwlock_ptr_eq; -use super::option_arc_rwlock_ptr_eq; - #[derive(Debug, Serialize, Deserialize, Default, Clone)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] /// Represents a message sent in a channel. diff --git a/src/types/schema/channel.rs b/src/types/schema/channel.rs index fea2545a..62b3b53a 100644 --- a/src/types/schema/channel.rs +++ b/src/types/schema/channel.rs @@ -138,29 +138,6 @@ bitflags! { } } -#[cfg(feature = "sqlx")] -impl sqlx::Type for InviteFlags { - fn type_info() -> sqlx::mysql::MySqlTypeInfo { - u64::type_info() - } -} - -#[cfg(feature = "sqlx")] -impl<'q> sqlx::Encode<'q, sqlx::MySql> for InviteFlags { - fn encode_by_ref(&self, buf: &mut >::ArgumentBuffer) -> sqlx::encode::IsNull { - u64::encode_by_ref(&self.0.0, buf) - } -} - -#[cfg(feature = "sqlx")] -impl<'r> sqlx::Decode<'r, sqlx::MySql> for InviteFlags { - fn decode(value: >::ValueRef) -> Result { - let raw = u64::decode(value)?; - - Ok(Self::from_bits(raw).unwrap()) - } -} - #[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] diff --git a/src/types/schema/role.rs b/src/types/schema/role.rs index 17292fea..805d0263 100644 --- a/src/types/schema/role.rs +++ b/src/types/schema/role.rs @@ -4,7 +4,6 @@ use crate::types::{PermissionFlags, Snowflake}; use serde::{Deserialize, Serialize}; -use crate::types::{PermissionFlags, Snowflake}; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] diff --git a/src/types/utils/serde.rs b/src/types/utils/serde.rs index f682bb0b..da41f4ac 100644 --- a/src/types/utils/serde.rs +++ b/src/types/utils/serde.rs @@ -32,7 +32,7 @@ pub struct SecondsStringTimestampVisitor; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); /// let my_s: S = serde_json::from_str(&as_string)?; /// assert_eq!(my_s.time, time); -/// // Ok::<(), serde_json::Error>(()) +/// # Ok::<(), serde_json::Error>(()) /// ``` pub mod ts_seconds_str { @@ -64,7 +64,7 @@ pub mod ts_seconds_str { /// }; /// let as_string = serde_json::to_string(&my_s)?; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); - /// // Ok::<(), serde_json::Error>(()) + /// # Ok::<(), serde_json::Error>(()) /// ``` pub fn serialize(dt: &DateTime, serializer: S) -> Result where @@ -91,7 +91,7 @@ pub mod ts_seconds_str { /// /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).unwrap() }); - /// // Ok::<(), serde_json::Error>(()) + /// # Ok::<(), serde_json::Error>(()) /// ``` pub fn deserialize<'de, D>(d: D) -> Result, D::Error> where @@ -145,7 +145,7 @@ pub mod ts_seconds_str { /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); /// let my_s: S = serde_json::from_str(&as_string)?; /// assert_eq!(my_s.time, time); -/// // Ok::<(), serde_json::Error>(()) +/// # Ok::<(), serde_json::Error>(()) /// ``` pub mod ts_seconds_option_str { use super::SecondsStringTimestampVisitor; @@ -174,7 +174,7 @@ pub mod ts_seconds_option_str { /// }; /// let as_string = serde_json::to_string(&my_s)?; /// assert_eq!(as_string, r#"{"time":"1431684000"}"#); - /// // Ok::<(), serde_json::Error>(()) + /// # Ok::<(), serde_json::Error>(()) /// ``` pub fn serialize(opt: &Option>, serializer: S) -> Result where @@ -204,7 +204,7 @@ pub mod ts_seconds_option_str { /// /// let my_s: S = serde_json::from_str(r#"{ "time": "1431684000" }"#)?; /// assert_eq!(my_s, S { time: Utc.timestamp_opt(1431684000, 0).single() }); - /// // Ok::<(), serde_json::Error>(()) + /// # Ok::<(), serde_json::Error>(()) /// ``` pub fn deserialize<'de, D>(d: D) -> Result>, D::Error> where From a2f9f9602a9f6efb643bbbd2f661a886620c0fe0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 10 Oct 2024 19:04:24 +0200 Subject: [PATCH 21/23] Bump version to 0.17.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 067322f9..30d7b2a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,7 +230,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chorus" -version = "0.16.0" +version = "0.17.0" dependencies = [ "async-trait", "base64 0.21.7", diff --git a/Cargo.toml b/Cargo.toml index 031aeff1..675a4662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "chorus" description = "A library for interacting with multiple Spacebar-compatible Instances at once." -version = "0.16.0" +version = "0.17.0" license = "MPL-2.0" edition = "2021" repository = "https://github.com/polyphony-chat/chorus" diff --git a/README.md b/README.md index afed9b2c..cdac65d4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ To get started with Chorus, import it into your project by adding the following ```toml [dependencies] -chorus = "0.16.0" +chorus = "0.17.0" ``` ### Establishing a Connection From b82751906e776a06a0038a029a467f2c5c92b0fe Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 11 Oct 2024 23:42:37 +0200 Subject: [PATCH 22/23] add docstring for sessions property --- src/types/events/ready.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index d8c11de1..3c512e87 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -38,6 +38,7 @@ pub struct GatewayReady { pub guilds: Vec, /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability). pub presences: Option>, + /// Undocumented. Seems to be a list of sessions the user is currently connected with. pub sessions: Option>, /// Unique session ID, used for resuming connections pub session_id: String, From 0938dd86ea440252c8c1f01fb8f1217a72972121 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 11 Oct 2024 23:45:50 +0200 Subject: [PATCH 23/23] append: documentation --- src/types/events/ready.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/events/ready.rs b/src/types/events/ready.rs index 3c512e87..f653b518 100644 --- a/src/types/events/ready.rs +++ b/src/types/events/ready.rs @@ -39,6 +39,7 @@ pub struct GatewayReady { /// The presences of the user's non-offline friends and implicit relationships (depending on the `NO_AFFINE_USER_IDS` Gateway capability). pub presences: Option>, /// Undocumented. Seems to be a list of sessions the user is currently connected with. + /// On Discord.com, this includes the current session. pub sessions: Option>, /// Unique session ID, used for resuming connections pub session_id: String,