From 92bface6ae37a530d261b517171abbc8f30ee2ed Mon Sep 17 00:00:00 2001 From: Paul <paulmakles@gmail.com> Date: Sat, 1 May 2021 22:55:37 +0100 Subject: [PATCH] Add group icons / profile backgrounds. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/database/entities/channel.rs | 5 ++ src/database/entities/user.rs | 12 ++-- src/notifications/payload.rs | 2 +- src/routes/channels/edit_channel.rs | 44 ++++++++++-- src/routes/channels/group_create.rs | 1 + src/routes/root.rs | 2 +- src/routes/users/edit_user.rs | 102 ++++++++++++++++++++++++---- 9 files changed, 145 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 20bc4ec..05168cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,7 +2475,7 @@ dependencies = [ [[package]] name = "revolt" -version = "0.4.1-alpha.2" +version = "0.4.1-alpha.3" dependencies = [ "async-std", "async-tungstenite", diff --git a/Cargo.toml b/Cargo.toml index ba34709..9073c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt" -version = "0.4.1-alpha.2" +version = "0.4.1-alpha.3" authors = ["Paul Makles <paulmakles@gmail.com>"] edition = "2018" diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index 6eff356..c1fd37b 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -28,6 +28,7 @@ pub enum Channel { DirectMessage { #[serde(rename = "_id")] id: String, + active: bool, recipients: Vec<String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -38,10 +39,14 @@ pub enum Channel { id: String, #[serde(skip_serializing_if = "Option::is_none")] nonce: Option<String>, + name: String, owner: String, description: String, recipients: Vec<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + icon: Option<File>, #[serde(skip_serializing_if = "Option::is_none")] last_message: Option<LastMessage>, }, diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index fe8ec39..96db17d 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -45,18 +45,20 @@ pub enum Presence { pub struct UserStatus { #[validate(length(min = 1, max = 128))] #[serde(skip_serializing_if = "Option::is_none")] - text: Option<String>, + pub text: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] - presence: Option<Presence>, + pub presence: Option<Presence>, } -#[derive(Validate, Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct UserProfile { - #[validate(length(min = 1, max = 2000))] #[serde(skip_serializing_if = "Option::is_none")] - content: Option<String>, + pub content: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option<File>, } +// When changing this struct, update notifications/payload.rs#80 #[derive(Serialize, Deserialize, Debug)] pub struct User { #[serde(rename = "_id")] diff --git a/src/notifications/payload.rs b/src/notifications/payload.rs index 73cf789..bd36e7a 100644 --- a/src/notifications/payload.rs +++ b/src/notifications/payload.rs @@ -77,7 +77,7 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> { } }, FindOptions::builder() - .projection(doc! { "_id": 1, "username": 1, "badges": 1, "status": 1 }) + .projection(doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1 }) .build(), ) .await diff --git a/src/routes/channels/edit_channel.rs b/src/routes/channels/edit_channel.rs index ebd573f..4e1d971 100644 --- a/src/routes/channels/edit_channel.rs +++ b/src/routes/channels/edit_channel.rs @@ -15,14 +15,17 @@ pub struct Data { #[validate(length(min = 0, max = 1024))] #[serde(skip_serializing_if = "Option::is_none")] description: Option<String>, + #[validate(length(min = 1, max = 128))] + icon: Option<String>, } #[patch("/<target>", data = "<info>")] pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> { + let info = info.into_inner(); info.validate() .map_err(|error| Error::FailedValidation { error })?; - if info.name.is_none() && info.description.is_none() { + if info.name.is_none() && info.description.is_none() && info.icon.is_none() { return Ok(()); } @@ -37,11 +40,34 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> { } match &target { - Channel::Group { id, .. } => { + Channel::Group { id, icon, .. } => { + let mut set = doc! {}; + if let Some(name) = &info.name { + set.insert("name", name); + } + + if let Some(description) = info.description { + set.insert("description", description); + } + + let mut remove_icon = false; + if let Some(attachment_id) = info.icon { + let attachment = File::find_and_use(&attachment_id, "icons", "object", &user.id).await?; + set.insert( + "icon", + to_document(&attachment).map_err(|_| Error::DatabaseError { + operation: "to_document", + with: "attachment", + })?, + ); + + remove_icon = true; + } + get_collection("channels") .update_one( doc! { "_id": &id }, - doc! { "$set": to_document(&info.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? }, + doc! { "$set": &set }, None ) .await @@ -49,18 +75,18 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> { ClientboundNotification::ChannelUpdate { id: id.clone(), - data: json!(info.0), + data: json!(set), } .publish(id.clone()) .await .ok(); - if let Some(name) = &info.name { + if let Some(name) = info.name { Message::create( "00000000000000000000000000".to_string(), id.clone(), Content::SystemMessage(SystemMessage::ChannelRenamed { - name: name.clone(), + name, by: user.id, }), ) @@ -69,6 +95,12 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> { .ok(); } + if remove_icon { + if let Some(old_icon) = icon { + old_icon.delete().await?; + } + } + Ok(()) } _ => Err(Error::InvalidOperation), diff --git a/src/routes/channels/group_create.rs b/src/routes/channels/group_create.rs index c979b01..ab63741 100644 --- a/src/routes/channels/group_create.rs +++ b/src/routes/channels/group_create.rs @@ -70,6 +70,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> { .unwrap_or_else(|| "A group.".to_string()), owner: user.id, recipients: set.into_iter().collect::<Vec<String>>(), + icon: None, last_message: None, }; diff --git a/src/routes/root.rs b/src/routes/root.rs index 669b08c..6dc77ec 100644 --- a/src/routes/root.rs +++ b/src/routes/root.rs @@ -9,7 +9,7 @@ use rocket_contrib::json::JsonValue; #[get("/")] pub async fn root() -> JsonValue { json!({ - "revolt": "0.4.1-alpha.2", + "revolt": "0.4.1-alpha.3", "features": { "registration": !*DISABLE_REGISTRATION, "captcha": { diff --git a/src/routes/users/edit_user.rs b/src/routes/users/edit_user.rs index 0c4a7da..ee5db91 100644 --- a/src/routes/users/edit_user.rs +++ b/src/routes/users/edit_user.rs @@ -7,34 +7,100 @@ use rocket_contrib::json::Json; use serde::{Deserialize, Serialize}; use validator::Validate; +#[derive(Validate, Serialize, Deserialize, Debug)] +pub struct UserProfileData { + #[validate(length(min = 0, max = 2000))] + #[serde(skip_serializing_if = "Option::is_none")] + content: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(min = 1, max = 128))] + background: Option<String>, +} + +#[derive(Serialize, Deserialize)] +pub enum RemoveField { + ProfileContent, + ProfileBackground, + StatusText, + Avatar +} + #[derive(Validate, Serialize, Deserialize)] pub struct Data { - #[serde(skip_serializing_if = "Option::is_none")] #[validate] status: Option<UserStatus>, - #[serde(skip_serializing_if = "Option::is_none")] #[validate] - profile: Option<UserProfile>, - #[serde(skip_serializing_if = "Option::is_none")] + profile: Option<UserProfileData>, #[validate(length(min = 1, max = 128))] avatar: Option<String>, + remove: Option<RemoveField> } #[patch("/<_ignore_id>", data = "<data>")] -pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result<()> { - if data.0.status.is_none() && data.0.profile.is_none() && data.0.avatar.is_none() { +pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> { + let mut data = data.into_inner(); + if data.status.is_none() && data.profile.is_none() && data.avatar.is_none() { return Ok(()); } data.validate() .map_err(|error| Error::FailedValidation { error })?; - let mut set = to_document(&data.0).map_err(|_| Error::DatabaseError { - operation: "to_document", - with: "data", - })?; + let mut unset = doc! {}; + let mut set = doc! {}; + + let mut remove_background = false; + let mut remove_avatar = false; + + if let Some(remove) = data.remove { + match remove { + RemoveField::ProfileContent => { + unset.insert("profile.content", 1); + }, + RemoveField::ProfileBackground => { + unset.insert("profile.background", 1); + remove_background = true; + } + RemoveField::StatusText => { + unset.insert("status.text", 1); + }, + RemoveField::Avatar => { + unset.insert("avatar", 1); + remove_avatar = true; + } + } + } - let avatar = std::mem::replace(&mut data.0.avatar, None); + if let Some(status) = &data.status { + set.insert( + "status", + to_document(&status).map_err(|_| Error::DatabaseError { + operation: "to_document", + with: "status", + })?, + ); + } + + if let Some(profile) = data.profile { + if let Some(content) = profile.content { + set.insert("profile.content", content); + } + + if let Some(attachment_id) = profile.background { + let attachment = File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?; + set.insert( + "profile.background", + to_document(&attachment).map_err(|_| Error::DatabaseError { + operation: "to_document", + with: "attachment", + })?, + ); + + remove_background = true; + } + } + + let avatar = std::mem::replace(&mut data.avatar, None); let attachment = if let Some(attachment_id) = avatar { let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?; set.insert( @@ -44,6 +110,8 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result with: "attachment", })?, ); + + remove_avatar = true; Some(attachment) } else { None @@ -57,7 +125,7 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result with: "user", })?; - if let Some(status) = data.0.status { + if let Some(status) = data.status { ClientboundNotification::UserUpdate { id: user.id.clone(), data: json!({ "status": status }), @@ -75,11 +143,21 @@ pub async fn req(user: User, mut data: Json<Data>, _ignore_id: String) -> Result .publish(user.id.clone()) .await .ok(); + } + if remove_avatar { if let Some(old_avatar) = user.avatar { old_avatar.delete().await?; } } + if remove_background { + if let Some(profile) = user.profile { + if let Some(old_background) = profile.background { + old_background.delete().await?; + } + } + } + Ok(()) } -- GitLab