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