diff --git a/Cargo.toml b/Cargo.toml index b17d024..5d956f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1", features = ["derive"] } serde_with = { version = "3", features = ["time_0_3"] } time = { version = "0.3", default-features = false, features = ["macros", "serde-well-known"] } serde_json = { version = "1" } -strum = { version = "0.25", features = ["derive"] } +strum = { version = "0.26", features = ["derive"] } # API client specifics racal = "0.3" @@ -40,7 +40,7 @@ percent-encoding = { version = "2", optional = true } base64 = { version = "0.21", optional = true } async-trait = { version = "0.1", optional = true } -# either = { version = "1", features = ["serde"] } +either = { version = "1", features = ["serde"] } url = { version = "2", features = ["serde"] } [dependencies.reqwest] optional = true diff --git a/README.md b/README.md index 999481a..ad49b7d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ There's quite a bit missing, PRs are welcome to improve the situation, even beyo | None | Favorites | Eventual implementation | | None | Files | None | | Partial | Friends | Implementation soon | -| None | Groups | None, at least in the near term | +| Partial | Groups | More testing | | None | Invites | Listing invites only | | Partial | Instances | Implementation soon | | None | Notifications | Eventual implementation | diff --git a/src/id.rs b/src/id.rs index a5fbe7a..cacbe1a 100644 --- a/src/id.rs +++ b/src/id.rs @@ -70,10 +70,11 @@ macro_rules! add_id { } add_id!(Avatar); -add_id!(User); +add_id!(Group); add_id!(Instance); -add_id!(World); add_id!(UnityPackage); +add_id!(User); +add_id!(World); /// Offline or the id of the world or whatever type T is #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] @@ -137,14 +138,16 @@ impl OfflineOrPrivateOr { pub enum Any { /// An avatar ID Avatar(Avatar), - /// An user ID - User(User), + /// An group ID + Group(Group), /// An instance ID Instance(Instance), - /// A world ID - World(World), /// An ID for an Unity package UnityPackage(UnityPackage), + /// An user ID + User(User), + /// A world ID + World(World), } impl AsRef for Any { @@ -153,10 +156,11 @@ impl AsRef for Any { fn as_ref(&self) -> &str { match self { Self::Avatar(v) => v.as_ref(), - Self::User(v) => v.as_ref(), + Self::Group(v) => v.as_ref(), Self::Instance(v) => v.as_ref(), - Self::World(v) => v.as_ref(), Self::UnityPackage(v) => v.as_ref(), + Self::User(v) => v.as_ref(), + Self::World(v) => v.as_ref(), } } } diff --git a/src/model/groups.rs b/src/model/groups.rs new file mode 100644 index 0000000..1d01370 --- /dev/null +++ b/src/model/groups.rs @@ -0,0 +1,116 @@ +use either::Either; +use serde::{Deserialize, Serialize}; + +use crate::id::User; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Details about a VRC group +pub struct Group { + /// The unique identifier for the group. + pub id: String, + /// The name of the group. + pub name: String, + /// The short code associated with the group. + pub short_code: String, + /// The discriminator for the group. + pub discriminator: String, + /// The description of the group. + pub description: String, + /// The unique identifier for the group's icon. + pub icon_id: String, + /// The URL of the group's icon. + pub icon_url: String, + /// The unique identifier for the group's banner. + pub banner_id: String, + /// The URL of the group's banner. + pub banner_url: String, + /// The privacy setting of the group. + pub privacy: String, + /// The unique identifier of the owner of the group. + pub owner_id: String, + /// The rules associated with the group. + pub rules: String, + /// The list of links associated with the group. + pub links: Vec, + /// The list of languages associated with the group. + pub languages: Vec, + /// The count of members in the group. + pub member_count: i64, + /// The times tamp when the member count was last synchronized. + pub member_count_synced_at: String, + /// Indicates whether the group is verified. + pub is_verified: bool, + /// The join state of the group. + pub join_state: String, + // pub tags: Vec, + // pub galleries: Vec, + /// The time stamp when the group was created. + pub created_at: String, + /// The count of online members in the group. + pub online_member_count: i64, + /// The membership status of the user. + pub membership_status: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Represents a collection of group audit logs. +pub struct GroupAuditLogs { + /// The list of group audit logs. + pub results: Vec, + /// The total count of audit logs. + pub total_count: u32, + /// Indicates whether there are more audit logs. + pub has_next: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Represents a single group audit log entry. +pub struct GroupAuditLog { + /// The unique identifier for the audit log entry. + pub id: String, + /// The time stamp when the audit log entry was created. + #[serde(rename = "created_at")] + pub created_at: String, + /// The unique identifier of the group associated with the audit log. + pub group_id: String, + /// The unique identifier of the actor who performed the action. + pub actor_id: User, + /// The display name of the actor. + pub actor_displayname: Option, + /// The unique identifier of the target of the action. + pub target_id: Option, + /// The type of event captured in the audit log. + pub event_type: String, + /// The description of the event captured in the audit log. + pub description: String, + /// Additional data associated with the audit log entry. + pub data: GroupAuditLogData, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Represents additional data associated with a group audit log entry. +pub struct GroupAuditLogData { + /// The description change associated with the audit log entry. + #[serde(default, with = "either::serde_untagged_optional")] + pub description: Option, String>>, + /// The join state change associated with the audit log entry. + #[serde(default, with = "either::serde_untagged_optional")] + pub join_state: Option, String>>, + /// The order change associated with the audit log entry. + #[serde(default, with = "either::serde_untagged_optional")] + pub order: Option, u32>>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +/// Represents a change in field associated with a group audit log entry. +pub struct GroupAuditLogDataChange { + /// The old field before the change. + pub old: T, + /// The new field after the change. + pub new: T, +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 4b64ccb..9174bac 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -10,6 +10,8 @@ mod assets; pub use assets::*; mod auth; pub use auth::*; +mod groups; +pub use groups::*; mod instances; pub use instances::*; mod notifications; diff --git a/src/model/users.rs b/src/model/users.rs index ed8cbb2..c5c48d6 100644 --- a/src/model/users.rs +++ b/src/model/users.rs @@ -1,3 +1,4 @@ +use either::Either; use serde::{Deserialize, Serialize}; use time::{serde::rfc3339, OffsetDateTime}; use url::Url; @@ -280,7 +281,7 @@ pub struct CurrentAccountData { pub obfuscated_pending_email: String, /// Can be empty #[serde(default)] - pub past_display_names: Vec, + pub past_display_names: Vec>, /// If hasn't set status yet pub status_first_time: bool, /// History of statuses (VRC pre-populates some for new accounts) diff --git a/src/query/groups.rs b/src/query/groups.rs new file mode 100644 index 0000000..4547390 --- /dev/null +++ b/src/query/groups.rs @@ -0,0 +1,53 @@ +use racal::Queryable; +use serde::{Deserialize, Serialize}; + +use super::Authentication; + +/// Gets information about a specific group +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct Group { + /// The ID of the group to get information about + pub id: crate::id::Group, +} + +impl Queryable for Group { + fn url(&self, _: &Authentication) -> String { + format!("{}/groups/{}", crate::API_BASE_URI, self.id.as_ref()) + } +} + +/// Gets audit logs from a specific group +#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct GroupAuditLogs { + /// The ID of the group to get audit logs from + pub id: crate::id::Group, + /// The count of how many logs to get (1 - 100) + pub n: Option, + /// The offset of how many logs to get + pub offset: Option, + // TODO: startDate & endDate +} + +impl Queryable + for GroupAuditLogs +{ + fn url(&self, _: &Authentication) -> String { + let base_query = + format!("{}/groups/{}/auditLogs", crate::API_BASE_URI, self.id.as_ref()); + + let mut params = Vec::new(); + if let Some(n) = self.n { + params.push(format!("n={n}")); + } + + if let Some(offset) = self.offset { + params.push(format!("offset={offset}")); + } + + if params.is_empty() { + base_query + } else { + format!("{}?{}", base_query, params.join("&")) + } + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index 9bd0258..e385f2b 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -9,6 +9,8 @@ mod auth; pub use auth::*; mod friends; pub use friends::*; +mod groups; +pub use groups::*; mod instances; pub use instances::*; mod users; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 7de6755..d46ce1a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -19,6 +19,8 @@ pub struct TestConfig { pub friend_id: Option, pub self_id: Option, pub world_id: Option, + pub group_id: Option, + pub moderating_group_id: Option, } pub static TEST_CONFIG: Lazy = Lazy::new(|| { diff --git a/tests/groups.rs b/tests/groups.rs new file mode 100644 index 0000000..4a78072 --- /dev/null +++ b/tests/groups.rs @@ -0,0 +1,47 @@ +#![cfg(feature = "api_client")] + +use std::default; + +use vrc::{ + api_client::{ApiClient, ApiError}, + model::{Group, GroupAuditLogs}, +}; + +mod common; + +#[tokio::test] +#[ignore] +async fn group() -> Result<(), ApiError> { + let group_id = match &common::TEST_CONFIG.group_id { + Some(v) => v, + None => return Ok(()), + }; + + let api_client = common::api_client()?; + + let query = vrc::query::Group { id: group_id.clone() }; + let group: Group = api_client.query(query).await?; + + dbg!(&group); + + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn group_audit_logs() -> Result<(), ApiError> { + let group_id = match &common::TEST_CONFIG.moderating_group_id { + Some(v) => v, + None => return Ok(()), + }; + + let api_client = common::api_client()?; + + let query = + vrc::query::GroupAuditLogs { id: group_id.clone(), n: None, offset: None }; + let audit_logs: GroupAuditLogs = api_client.query(query).await?; + + dbg!(&audit_logs); + + Ok(()) +}