From f8c8407af392fc35bad9ccd6241fb3a04c8e4198 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 23 Apr 2023 17:44:36 +0100 Subject: [PATCH] feat(core/models): basic implementation of User and File models --- crates/core/database/src/models/bots/model.rs | 7 - .../database/src/models/bots/ops/mongodb.rs | 1 - crates/core/models/Cargo.toml | 3 +- crates/core/models/src/bots.rs | 49 ++-- crates/core/models/src/files.rs | 95 +++++++ crates/core/models/src/lib.rs | 9 + crates/core/models/src/users.rs | 233 ++++++++++++++++++ crates/delta/src/routes/bots/fetch.rs | 26 +- crates/delta/src/routes/bots/fetch_public.rs | 13 +- justfile | 1 + 10 files changed, 387 insertions(+), 50 deletions(-) create mode 100644 crates/core/models/src/files.rs create mode 100644 crates/core/models/src/users.rs diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs index a8c551e0..1a9fa17e 100644 --- a/crates/core/database/src/models/bots/model.rs +++ b/crates/core/database/src/models/bots/model.rs @@ -42,13 +42,6 @@ auto_derived_partial!( ); auto_derived!( - /// Flags that may be attributed to a bot - #[repr(i32)] - pub enum BotFlags { - Verified = 1, - Official = 2, - } - /// Optional fields on bot object pub enum FieldsBot { Token, diff --git a/crates/core/database/src/models/bots/ops/mongodb.rs b/crates/core/database/src/models/bots/ops/mongodb.rs index bda3ae02..69da814f 100644 --- a/crates/core/database/src/models/bots/ops/mongodb.rs +++ b/crates/core/database/src/models/bots/ops/mongodb.rs @@ -1,4 +1,3 @@ -use ::mongodb::bson::doc; use revolt_result::Result; use crate::{Bot, FieldsBot, PartialBot}; diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 152363ad..409f2278 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -11,13 +11,14 @@ description = "Revolt Backend: API Models" [features] serde = [ "dep:serde" ] schemas = [ "dep:schemars" ] -from_database = [ "revolt-database" ] +from_database = [ "revolt-database", "revolt-presence" ] default = [ "serde", "from_database" ] [dependencies] # Repo revolt-database = { version = "0.0.1", path = "../database", optional = true } +revolt-presence = { version = "0.0.1", path = "../presence", optional = true } # Serialisation serde = { version = "1", features = ["derive"], optional = true } diff --git a/crates/core/models/src/bots.rs b/crates/core/models/src/bots.rs index 7c201e68..f558087f 100644 --- a/crates/core/models/src/bots.rs +++ b/crates/core/models/src/bots.rs @@ -1,5 +1,7 @@ +use crate::User; + auto_derived!( - /// # Bot + /// Bot pub struct Bot { /// Bot Id #[serde(rename = "_id")] @@ -46,11 +48,21 @@ auto_derived!( pub privacy_policy_url: String, /// Enum of bot flags - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub flags: Option, + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "crate::if_zero_u32", default) + )] + pub flags: u32, } - /// # Public Bot + /// Flags that may be attributed to a bot + #[repr(u32)] + pub enum BotFlags { + Verified = 1, + Official = 2, + } + + /// Public Bot pub struct PublicBot { /// Bot Id #[serde(rename = "_id")] @@ -65,21 +77,30 @@ auto_derived!( #[serde(skip_serializing_if = "String::is_empty")] description: String, } + + /// Bot Response + pub struct FetchBotResponse { + /// Bot object + pub bot: Bot, + /// User object + pub user: User, + } ); #[cfg(feature = "from_database")] impl PublicBot { - pub fn from( - bot: revolt_database::Bot, - username: String, - avatar: Option, - description: Option, - ) -> Self { + pub fn from(bot: revolt_database::Bot, user: revolt_database::User) -> Self { + #[cfg(debug_assertions)] + assert_eq!(bot.id, user.id); + PublicBot { id: bot.id, - username, - avatar: avatar.unwrap_or_default(), - description: description.unwrap_or_default(), + username: user.username, + avatar: user.avatar.map(|x| x.id).unwrap_or_default(), + description: user + .profile + .map(|profile| profile.content) + .unwrap_or_default(), } } } @@ -97,7 +118,7 @@ impl From for Bot { interactions_url: value.interactions_url, terms_of_service_url: value.terms_of_service_url, privacy_policy_url: value.privacy_policy_url, - flags: value.flags, + flags: value.flags.unwrap_or_default() as u32, } } } diff --git a/crates/core/models/src/files.rs b/crates/core/models/src/files.rs new file mode 100644 index 00000000..89654b75 --- /dev/null +++ b/crates/core/models/src/files.rs @@ -0,0 +1,95 @@ +auto_derived!( + /// File + pub struct File { + /// Unique Id + #[serde(rename = "_id")] + pub id: String, + /// Tag / bucket this file was uploaded to + pub tag: String, + /// Original filename + pub filename: String, + /// Parsed metadata of this file + pub metadata: Metadata, + /// Raw content type of this file + pub content_type: String, + /// Size of this file (in bytes) + pub size: isize, + + /// Whether this file was deleted + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted: Option, + /// Whether this file was reported + #[serde(skip_serializing_if = "Option::is_none")] + pub reported: Option, + + // TODO: migrate this mess to having: + // - author_id + // - parent: Parent { Message(id), User(id), etc } + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub server_id: Option, + + /// Id of the object this file is associated with + #[serde(skip_serializing_if = "Option::is_none")] + pub object_id: Option, + } + + /// Metadata associated with a file + #[serde(tag = "type")] + #[derive(Default)] + pub enum Metadata { + /// File is just a generic uncategorised file + #[default] + File, + /// File contains textual data and should be displayed as such + Text, + /// File is an image with specific dimensions + Image { width: usize, height: usize }, + /// File is a video with specific dimensions + Video { width: usize, height: usize }, + /// File is audio + Audio, + } +); + +#[cfg(feature = "from_database")] +impl From for File { + fn from(value: revolt_database::File) -> Self { + File { + id: value.id, + tag: value.tag, + filename: value.filename, + metadata: value.metadata.into(), + content_type: value.content_type, + size: value.size, + deleted: value.deleted, + reported: value.reported, + message_id: value.message_id, + user_id: value.user_id, + server_id: value.server_id, + object_id: value.object_id, + } + } +} + +#[cfg(feature = "from_database")] +impl From for Metadata { + fn from(value: revolt_database::Metadata) -> Self { + match value { + revolt_database::Metadata::File => Metadata::File, + revolt_database::Metadata::Text => Metadata::Text, + revolt_database::Metadata::Image { width, height } => Metadata::Image { + width: width as usize, + height: height as usize, + }, + revolt_database::Metadata::Video { width, height } => Metadata::Video { + width: width as usize, + height: height as usize, + }, + revolt_database::Metadata::Audio => Metadata::Audio, + } + } +} diff --git a/crates/core/models/src/lib.rs b/crates/core/models/src/lib.rs index 21f199c8..8d8b50d5 100644 --- a/crates/core/models/src/lib.rs +++ b/crates/core/models/src/lib.rs @@ -18,10 +18,19 @@ macro_rules! auto_derived { } mod bots; +mod files; +mod users; pub use bots::*; +pub use files::*; +pub use users::*; /// Utility function to check if a boolean value is false pub fn if_false(t: &bool) -> bool { !t } + +/// Utility function to check if an u32 is zero +pub fn if_zero_u32(t: &u32) -> bool { + t == &0 +} diff --git a/crates/core/models/src/users.rs b/crates/core/models/src/users.rs new file mode 100644 index 00000000..09d3b0b4 --- /dev/null +++ b/crates/core/models/src/users.rs @@ -0,0 +1,233 @@ +use crate::File; + +auto_derived!( + /// User + pub struct User { + /// Unique Id + #[serde(rename = "_id")] + pub id: String, + /// Username + pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// Avatar attachment + pub avatar: Option, + /// Relationships with other users + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub relations: Vec, + + /// Bitfield of user badges + #[serde(skip_serializing_if = "crate::if_zero_u32", default)] + pub badges: u32, + /// User's current status + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, + /// User's profile page + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option, + + /// Enum of user flags + #[serde(skip_serializing_if = "crate::if_zero_u32", default)] + pub flags: u32, + /// Whether this user is privileged + #[serde(skip_serializing_if = "crate::if_false", default)] + pub privileged: bool, + /// Bot information + #[serde(skip_serializing_if = "Option::is_none")] + pub bot: Option, + + /// Current session user's relationship with this user + pub relationship: RelationshipStatus, + /// Whether this user is currently online + pub online: bool, + } + + /// User's relationship with another user (or themselves) + #[derive(Default)] + pub enum RelationshipStatus { + #[default] + None, + User, + Friend, + Outgoing, + Incoming, + Blocked, + BlockedOther, + } + + /// Relationship entry indicating current status with other user + pub struct Relationship { + #[serde(rename = "_id")] + pub user_id: String, + pub status: RelationshipStatus, + } + + /// Presence status + pub enum Presence { + /// User is online + Online, + /// User is not currently available + Idle, + /// User is focusing / will only receive mentions + Focus, + /// User is busy / will not receive any notifications + Busy, + /// User appears to be offline + Invisible, + } + + /// User's active status + pub struct UserStatus { + /// Custom status text + #[serde(skip_serializing_if = "String::is_empty")] + pub text: String, + /// Current presence option + #[serde(skip_serializing_if = "Option::is_none")] + pub presence: Option, + } + + /// User's profile + pub struct UserProfile { + /// Text content on user's profile + #[serde(skip_serializing_if = "String::is_empty")] + pub content: String, + /// Background visible on user's profile + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, + } + + /// User badge bitfield + #[repr(u32)] + pub enum UserBadges { + /// Revolt Developer + Developer = 1, + /// Helped translate Revolt + Translator = 2, + /// Monetarily supported Revolt + Supporter = 4, + /// Responsibly disclosed a security issue + ResponsibleDisclosure = 8, + /// Revolt Founder + Founder = 16, + /// Platform moderator + PlatformModeration = 32, + /// Active monetary supporter + ActiveSupporter = 64, + /// 🦊🦝 + Paw = 128, + /// Joined as one of the first 1000 users in 2021 + EarlyAdopter = 256, + /// Amogus + ReservedRelevantJokeBadge1 = 512, + /// Low resolution troll face + ReservedRelevantJokeBadge2 = 1024, + } + + /// User flag enum + #[repr(u32)] + pub enum UserFlags { + /// User has been suspended from the platform + Suspended = 1, + /// User has deleted their account + Deleted = 2, + /// User was banned off the platform + Banned = 4, + /// User was marked as spam and removed from platform + Spam = 8, + } + + /// Bot information for if the user is a bot + pub struct BotInformation { + /// Id of the owner of this bot + #[serde(rename = "owner")] + pub owner_id: String, + } +); + +pub trait CheckRelationship { + fn with(&self, user: &str) -> RelationshipStatus; +} + +impl CheckRelationship for Vec { + fn with(&self, user: &str) -> RelationshipStatus { + for entry in self { + if entry.user_id == user { + return entry.status.clone(); + } + } + + RelationshipStatus::None + } +} + +#[cfg(feature = "from_database")] +impl User { + pub async fn from

(user: revolt_database::User, perspective: P) -> Self + where + P: Into>, + { + let relationship = if let Some(perspective) = perspective.into() { + perspective + .relations + .unwrap_or_default() + .into_iter() + .find(|relationship| relationship.id == user.id) + .map(|relationship| relationship.status.into()) + .unwrap_or_default() + } else { + RelationshipStatus::None + }; + + // do permission stuff here + // TODO: implement permissions =) + let can_see_profile = false; + + Self { + username: user.username, + avatar: user.avatar.map(|file| file.into()), + relations: vec![], + badges: user.badges.unwrap_or_default() as u32, + status: None, + profile: None, + flags: user.flags.unwrap_or_default() as u32, + privileged: user.privileged, + bot: user.bot.map(|bot| bot.into()), + relationship, + online: can_see_profile && revolt_presence::is_online(&user.id).await, + id: user.id, + } + } +} + +#[cfg(feature = "from_database")] +impl From for BotInformation { + fn from(value: revolt_database::BotInformation) -> Self { + BotInformation { + owner_id: value.owner, + } + } +} + +#[cfg(feature = "from_database")] +impl From for Relationship { + fn from(value: revolt_database::Relationship) -> Self { + Self { + user_id: value.id, + status: value.status.into(), + } + } +} + +#[cfg(feature = "from_database")] +impl From for RelationshipStatus { + fn from(value: revolt_database::RelationshipStatus) -> Self { + match value { + revolt_database::RelationshipStatus::None => RelationshipStatus::None, + revolt_database::RelationshipStatus::User => RelationshipStatus::User, + revolt_database::RelationshipStatus::Friend => RelationshipStatus::Friend, + revolt_database::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing, + revolt_database::RelationshipStatus::Incoming => RelationshipStatus::Incoming, + revolt_database::RelationshipStatus::Blocked => RelationshipStatus::Blocked, + revolt_database::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther, + } + } +} diff --git a/crates/delta/src/routes/bots/fetch.rs b/crates/delta/src/routes/bots/fetch.rs index ef4cb214..41aee968 100644 --- a/crates/delta/src/routes/bots/fetch.rs +++ b/crates/delta/src/routes/bots/fetch.rs @@ -1,18 +1,7 @@ use revolt_database::{util::reference::Reference, Database}; -use revolt_models::Bot; -use revolt_quark::{models::User, Db, Error, Result}; +use revolt_models::FetchBotResponse; +use revolt_quark::{models::User, Error, Result}; use rocket::{serde::json::Json, State}; -use serde::Serialize; - -/// # Bot Response -/// TODO: move to revolt-models -#[derive(Serialize, JsonSchema)] -pub struct BotResponse { - /// Bot object - bot: Bot, - /// User object - user: User, -} /// # Fetch Bot /// @@ -20,11 +9,10 @@ pub struct BotResponse { #[openapi(tag = "Bots")] #[get("/")] pub async fn fetch_bot( - legacy_db: &Db, db: &State, user: User, bot: Reference, -) -> Result> { +) -> Result> { if user.bot.is_some() { return Err(Error::IsBot); } @@ -34,8 +22,12 @@ pub async fn fetch_bot( return Err(Error::NotFound); } - Ok(Json(BotResponse { - user: legacy_db.fetch_user(&bot.id).await?.foreign(), + Ok(Json(FetchBotResponse { + user: revolt_models::User::from( + db.fetch_user(&bot.id).await.map_err(Error::from_core)?, + None, + ) + .await, bot: bot.into(), })) } diff --git a/crates/delta/src/routes/bots/fetch_public.rs b/crates/delta/src/routes/bots/fetch_public.rs index 1473eb65..87472f1c 100644 --- a/crates/delta/src/routes/bots/fetch_public.rs +++ b/crates/delta/src/routes/bots/fetch_public.rs @@ -1,6 +1,6 @@ use revolt_database::Database; use revolt_models::PublicBot; -use revolt_quark::{models::User, Db, Error, Ref, Result}; +use revolt_quark::{models::User, Error, Ref, Result}; use rocket::serde::json::Json; use rocket::State; @@ -11,7 +11,6 @@ use rocket::State; #[openapi(tag = "Bots")] #[get("//invite")] pub async fn fetch_public_bot( - legacy_db: &Db, db: &State, user: Option, target: Ref, @@ -21,12 +20,6 @@ pub async fn fetch_public_bot( return Err(Error::NotFound); } - let user = legacy_db.fetch_user(&bot.id).await?; - - Ok(Json(PublicBot::from( - bot, - user.username, - user.avatar.map(|f| f.id), - user.profile.and_then(|p| p.content), - ))) + let user = db.fetch_user(&bot.id).await.map_err(Error::from_core)?; + Ok(Json(PublicBot::from(bot, user))) } diff --git a/justfile b/justfile index 778aeaa4..2a75ff88 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ publish: cargo publish --package revolt-result cargo publish --package revolt-database + cargo publish --package revolt-presence cargo publish --package revolt-models