feat(core/database): implement user operations

This commit is contained in:
Paul Makles
2023-04-26 17:36:51 +01:00
parent e33ae17061
commit 08ccc43f8d
7 changed files with 537 additions and 6 deletions

1
Cargo.lock generated
View File

@@ -2854,6 +2854,7 @@ dependencies = [
"log",
"mongodb",
"nanoid",
"once_cell",
"revolt-permissions",
"revolt-result",
"revolt_optional_struct",

View File

@@ -27,6 +27,7 @@ revolt-permissions = { version = "0.0.2", path = "../permissions" }
# Utility
log = "0.4"
nanoid = "0.4.0"
once_cell = "1.17"
# Serialisation
serde_json = "1"

View File

@@ -84,7 +84,7 @@ impl Bot {
/// Delete this bot
pub async fn delete(&self, db: &Database) -> Result<()> {
// db.fetch_user(&self.id).await?.mark_deleted(db).await?;
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
db.delete_bot(&self.id).await
}
}

View File

@@ -1,4 +1,6 @@
use crate::File;
use crate::{Database, File};
use revolt_result::{Error, ErrorType, Result};
auto_derived_partial!(
/// # User
@@ -96,4 +98,99 @@ auto_derived!(
/// Id of the owner of this bot
pub owner: String,
}
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
);
impl User {
/// Check whether a username is already in use by another user
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
match db.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
Err(Error {
error_type: ErrorType::NotFound,
..
}) => Ok(false),
Err(error) => Err(error),
}
}
/// Update user data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_user(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::UserUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p_user(self.id.clone(), db)
.await; */
Ok(())
}
/// Remove a field from User object
pub fn remove_field(&mut self, field: &FieldsUser) {
match field {
FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() {
x.text = String::new();
}
}
FieldsUser::StatusPresence => {
if let Some(x) = self.status.as_mut() {
x.presence = None;
}
}
FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() {
x.content = String::new();
}
}
FieldsUser::ProfileBackground => {
if let Some(x) = self.profile.as_mut() {
x.background = None;
}
}
}
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
username: Some(format!("Deleted User {}", self.id)),
flags: Some(2),
..Default::default()
},
vec![
FieldsUser::Avatar,
FieldsUser::StatusText,
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
],
)
.await
}
}

View File

@@ -1,12 +1,57 @@
use revolt_result::Result;
use crate::User;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractUsers: Sync + Send {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()>;
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
user: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()>;
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
user_id: &str,
target_id: &str,
relationship: &RelationshipStatus,
) -> Result<()>;
/// Remove relationship with another user
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()>;
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()>;
}

View File

@@ -1,16 +1,300 @@
use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
use authifier::models::Session;
use futures::StreamExt;
use revolt_result::Result;
use crate::DocumentId;
use crate::IntoDocumentPath;
use crate::MongoDb;
use crate::User;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use super::AbstractUsers;
static COL: &str = "bots";
static COL: &str = "users";
#[async_trait]
impl AbstractUsers for MongoDb {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()> {
query!(self, insert_one, COL, &user).map(|_| ())
}
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
query!(
self,
find_one_with_options,
COL,
doc! {
"username": username
},
FindOneOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.build()
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
let session = self
.col::<Session>("sessions")
.find_one(
doc! {
"token": token
},
None,
)
.await
.map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?;
self.fetch_user(&session.id).await
}
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
Ok(self
.col::<User>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", "user"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>(COL)
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| create_database_error!("find", "users"))?
.filter_map(|s| async { s.ok() })
.map(|user| user.id)
.collect()
.await)
}
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("channels")
.find(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ user_a, user_b ]
}
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() })
.map(|user| user.id)
.collect()
.await)
}
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("server_members")
.aggregate(
vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
}
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
],
None,
)
.await
.map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() })
.filter_map(|doc| async move { doc.get_str("_id").map(|id| id.to_string()).ok() })
.collect()
.await)
}
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
user_id: &str,
target_id: &str,
relationship: &RelationshipStatus,
) -> Result<()> {
if let RelationshipStatus::None = relationship {
return self.pull_relationship(user_id, target_id).await;
}
self.col::<User>(COL)
.update_one(
doc! {
"_id": user_id
},
vec![doc! {
"$set": {
"relations": {
"$concatArrays": [
{
"$ifNull": [
{
"$filter": {
"input": "$relations",
"cond": {
"$ne": [
"$$this._id",
target_id
]
}
}
},
[]
]
},
[
{
"_id": target_id,
"status": format!("{relationship:?}")
}
]
]
}
}
}],
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user"))
}
/// Remove relationship with another user
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
self.col::<User>(COL)
.update_one(
doc! {
"_id": user_id
},
doc! {
"$pull": {
"relations": {
"_id": target_id
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user"))
}
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsUser {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsUser::Avatar => "avatar",
FieldsUser::ProfileBackground => "profile.background",
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
})
}
}

View File

@@ -1,12 +1,23 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::User;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use super::AbstractUsers;
#[async_trait]
impl AbstractUsers for ReferenceDb {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()> {
let mut users = self.users.lock().await;
if users.contains_key(&user.id) {
Err(create_database_error!("insert", "user"))
} else {
users.insert(user.id.to_string(), user.clone());
Ok(())
}
}
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User> {
let users = self.users.lock().await;
@@ -15,4 +26,96 @@ impl AbstractUsers for ReferenceDb {
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
users
.values()
.find(|user| user.username.to_lowercase() == lowercase)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
todo!()
}
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
let users = self.users.lock().await;
ids.iter()
.map(|id| {
users
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
let mut users = self.users.lock().await;
if let Some(user) = users.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
user.remove_field(&field);
}
user.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
_user_id: &str,
_target_id: &str,
_relationship: &RelationshipStatus,
) -> Result<()> {
todo!()
}
/// Remove relationship with another user
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> {
todo!()
}
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()> {
let mut users = self.users.lock().await;
if users.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}