feat(core/models): basic implementation of User and File models

This commit is contained in:
Paul Makles
2023-04-23 17:44:36 +01:00
parent e6d0d44c5a
commit f8c8407af3
10 changed files with 387 additions and 50 deletions

View File

@@ -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,

View File

@@ -1,4 +1,3 @@
use ::mongodb::bson::doc;
use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};

View File

@@ -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 }

View File

@@ -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<i32>,
#[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<String>,
description: Option<String>,
) -> 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<revolt_database::Bot> 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,
}
}
}

View File

@@ -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<bool>,
/// Whether this file was reported
#[serde(skip_serializing_if = "Option::is_none")]
pub reported: Option<bool>,
// 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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
/// Id of the object this file is associated with
#[serde(skip_serializing_if = "Option::is_none")]
pub object_id: Option<String>,
}
/// 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<revolt_database::File> 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<revolt_database::Metadata> 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,
}
}
}

View File

@@ -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
}

View File

@@ -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<File>,
/// Relationships with other users
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub relations: Vec<Relationship>,
/// 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<UserStatus>,
/// User's profile page
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<UserProfile>,
/// 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<BotInformation>,
/// 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<Presence>,
}
/// 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<File>,
}
/// 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<Relationship> {
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<P>(user: revolt_database::User, perspective: P) -> Self
where
P: Into<Option<revolt_database::User>>,
{
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<revolt_database::BotInformation> for BotInformation {
fn from(value: revolt_database::BotInformation) -> Self {
BotInformation {
owner_id: value.owner,
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Relationship> for Relationship {
fn from(value: revolt_database::Relationship) -> Self {
Self {
user_id: value.id,
status: value.status.into(),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::RelationshipStatus> 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,
}
}
}

View File

@@ -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("/<bot>")]
pub async fn fetch_bot(
legacy_db: &Db,
db: &State<Database>,
user: User,
bot: Reference,
) -> Result<Json<BotResponse>> {
) -> Result<Json<FetchBotResponse>> {
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(),
}))
}

View File

@@ -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("/<target>/invite")]
pub async fn fetch_public_bot(
legacy_db: &Db,
db: &State<Database>,
user: Option<User>,
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)))
}

View File

@@ -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