From 9a417aa6ad42370c889eaab49a4da2e358a41891 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Wed, 23 Jun 2021 13:50:18 +0100
Subject: [PATCH] Messaging: Add message replies. Servers: Add VoiceChannel.

---
 set_version.sh                             |   2 +-
 src/database/entities/channel.rs           | 170 ++++++++++++---------
 src/database/entities/message.rs           |   5 +
 src/database/permissions/channel.rs        |   4 +-
 src/notifications/events.rs                |   3 +-
 src/routes/channels/delete_channel.rs      |   3 +-
 src/routes/channels/invite_create.rs       |   3 +-
 src/routes/channels/message_delete.rs      |   1 +
 src/routes/channels/message_edit.rs        |   2 +
 src/routes/channels/message_fetch.rs       |   1 +
 src/routes/channels/message_query.rs       |   1 +
 src/routes/channels/message_query_stale.rs |   1 +
 src/routes/channels/message_send.rs        |  64 ++++++--
 src/routes/servers/channel_create.rs       |  43 ++++--
 src/util/result.rs                         |   4 +
 src/version.rs                             |   2 +-
 16 files changed, 208 insertions(+), 101 deletions(-)

diff --git a/set_version.sh b/set_version.sh
index d003f77..e84cc49 100755
--- a/set_version.sh
+++ b/set_version.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
-export version=0.5.0-alpha.5
+export version=0.5.1-alpha.0
 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs
index 5f102b2..97cee87 100644
--- a/src/database/entities/channel.rs
+++ b/src/database/entities/channel.rs
@@ -66,6 +66,19 @@ pub enum Channel {
         #[serde(skip_serializing_if = "Option::is_none")]
         last_message: Option<String>,
     },
+    VoiceChannel {
+        #[serde(rename = "_id")]
+        id: String,
+        server: String,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        nonce: Option<String>,
+
+        name: String,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        description: Option<String>,
+        #[serde(skip_serializing_if = "Option::is_none")]
+        icon: Option<File>,
+    },
 }
 
 impl Channel {
@@ -74,7 +87,17 @@ impl Channel {
             Channel::SavedMessages { id, .. }
             | Channel::DirectMessage { id, .. }
             | Channel::Group { id, .. }
-            | Channel::TextChannel { id, .. } => id,
+            | Channel::TextChannel { id, .. }
+            | Channel::VoiceChannel { id, .. } => id,
+        }
+    }
+    pub fn has_messaging(&self) -> Result<()> {
+        match self {
+            Channel::SavedMessages { .. }
+            | Channel::DirectMessage { .. }
+            | Channel::Group { .. }
+            | Channel::TextChannel { .. } => Ok(()),
+            Channel::VoiceChannel { .. } => Err(Error::InvalidOperation)
         }
     }
 
@@ -129,79 +152,84 @@ impl Channel {
                 with: "channel_invites",
             })?;
 
-        // Delete any unreads.
-        get_collection("channel_unreads")
-            .delete_many(
-                doc! {
-                    "_id.channel": id
-                },
-                None,
-            )
-            .await
-            .map_err(|_| Error::DatabaseError {
-                operation: "delete_many",
-                with: "channel_unreads",
-            })?;
+        match &self {
+            Channel::VoiceChannel { .. } => {},
+            _ => {
+                // Delete any unreads.
+                get_collection("channel_unreads")
+                    .delete_many(
+                        doc! {
+                            "_id.channel": id
+                        },
+                        None,
+                    )
+                    .await
+                    .map_err(|_| Error::DatabaseError {
+                        operation: "delete_many",
+                        with: "channel_unreads",
+                    })?;
 
-        // Check if there are any attachments we need to delete.
-        let message_ids = messages
-            .find(
-                doc! {
-                    "channel": id,
-                    "attachment": {
-                        "$exists": 1
-                    }
-                },
-                FindOptions::builder().projection(doc! { "_id": 1 }).build(),
-            )
-            .await
-            .map_err(|_| Error::DatabaseError {
-                operation: "fetch_many",
-                with: "messages",
-            })?
-            .filter_map(async move |s| s.ok())
-            .collect::<Vec<Document>>()
-            .await
-            .into_iter()
-            .filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
-            .collect::<Vec<String>>();
+                // Check if there are any attachments we need to delete.
+                let message_ids = messages
+                    .find(
+                        doc! {
+                            "channel": id,
+                            "attachment": {
+                                "$exists": 1
+                            }
+                        },
+                        FindOptions::builder().projection(doc! { "_id": 1 }).build(),
+                    )
+                    .await
+                    .map_err(|_| Error::DatabaseError {
+                        operation: "fetch_many",
+                        with: "messages",
+                    })?
+                    .filter_map(async move |s| s.ok())
+                    .collect::<Vec<Document>>()
+                    .await
+                    .into_iter()
+                    .filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
+                    .collect::<Vec<String>>();
 
-        // If we found any, mark them as deleted.
-        if message_ids.len() > 0 {
-            get_collection("attachments")
-                .update_many(
-                    doc! {
-                        "message_id": {
-                            "$in": message_ids
-                        }
-                    },
-                    doc! {
-                        "$set": {
-                            "deleted": true
-                        }
-                    },
-                    None,
-                )
-                .await
-                .map_err(|_| Error::DatabaseError {
-                    operation: "update_many",
-                    with: "attachments",
-                })?;
-        }
+                // If we found any, mark them as deleted.
+                if message_ids.len() > 0 {
+                    get_collection("attachments")
+                        .update_many(
+                            doc! {
+                                "message_id": {
+                                    "$in": message_ids
+                                }
+                            },
+                            doc! {
+                                "$set": {
+                                    "deleted": true
+                                }
+                            },
+                            None,
+                        )
+                        .await
+                        .map_err(|_| Error::DatabaseError {
+                            operation: "update_many",
+                            with: "attachments",
+                        })?;
+                }
 
-        // And then delete said messages.
-        messages
-            .delete_many(
-                doc! {
-                    "channel": id
-                },
-                None,
-            )
-            .await
-            .map_err(|_| Error::DatabaseError {
-                operation: "delete_many",
-                with: "messages",
-            })?;
+                // And then delete said messages.
+                messages
+                    .delete_many(
+                        doc! {
+                            "channel": id
+                        },
+                        None,
+                    )
+                    .await
+                    .map_err(|_| Error::DatabaseError {
+                        operation: "delete_many",
+                        with: "messages",
+                    })?;
+            }
+        }
 
         // Remove from server object.
         if let Channel::TextChannel { server, .. } = &self {
diff --git a/src/database/entities/message.rs b/src/database/entities/message.rs
index 675ad35..b733515 100644
--- a/src/database/entities/message.rs
+++ b/src/database/entities/message.rs
@@ -57,6 +57,7 @@ impl Content {
             target.id().to_string(),
             self,
             None,
+            None
         )
         .publish(&target)
         .await
@@ -81,6 +82,8 @@ pub struct Message {
     pub embeds: Option<Vec<Embed>>,
     #[serde(skip_serializing_if = "Option::is_none")]
     pub mentions: Option<Vec<String>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub replies: Option<Vec<String>>
 }
 
 impl Message {
@@ -89,6 +92,7 @@ impl Message {
         channel: String,
         content: Content,
         mentions: Option<Vec<String>>,
+        replies: Option<Vec<String>>,
     ) -> Message {
         Message {
             id: Ulid::new().to_string(),
@@ -101,6 +105,7 @@ impl Message {
             edited: None,
             embeds: None,
             mentions,
+            replies
         }
     }
 
diff --git a/src/database/permissions/channel.rs b/src/database/permissions/channel.rs
index c47a21b..e05f4ce 100644
--- a/src/database/permissions/channel.rs
+++ b/src/database/permissions/channel.rs
@@ -84,7 +84,8 @@ impl<'a> PermissionCalculator<'a> {
                     Ok(0)
                 }
             }
-            Channel::TextChannel { server, .. } => {
+            Channel::TextChannel { server, .. }
+            | Channel::VoiceChannel { server, .. } => {
                 let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
 
                 if self.perspective.id == server.owner {
@@ -92,6 +93,7 @@ impl<'a> PermissionCalculator<'a> {
                 } else {
                     Ok(ChannelPermission::View
                         + ChannelPermission::SendMessage
+                        + ChannelPermission::VoiceCall
                         + ChannelPermission::InviteOthers)
                 }
             }
diff --git a/src/notifications/events.rs b/src/notifications/events.rs
index b26b7c5..3cc091c 100644
--- a/src/notifications/events.rs
+++ b/src/notifications/events.rs
@@ -178,7 +178,8 @@ pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<()
                         subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
                     }
                 }
-                Channel::TextChannel { server, .. } => {
+                Channel::TextChannel { server, .. }
+                | Channel::VoiceChannel { server, .. } => {
                     // ! FIXME: write a better algorithm?
                     let members = Server::fetch_member_ids(server).await?;
                     for member in members {
diff --git a/src/routes/channels/delete_channel.rs b/src/routes/channels/delete_channel.rs
index d11d8fe..6bff3df 100644
--- a/src/routes/channels/delete_channel.rs
+++ b/src/routes/channels/delete_channel.rs
@@ -105,7 +105,8 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
 
             Ok(())
         }
-        Channel::TextChannel { .. } => {
+        Channel::TextChannel { .. } |
+        Channel::VoiceChannel { .. } => {
             if perm.get_manage_channel() {
                 target.delete().await
             } else {
diff --git a/src/routes/channels/invite_create.rs b/src/routes/channels/invite_create.rs
index eaef595..0018b3b 100644
--- a/src/routes/channels/invite_create.rs
+++ b/src/routes/channels/invite_create.rs
@@ -30,7 +30,8 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
         Channel::Group { .. } => {
             unimplemented!()
         }
-        Channel::TextChannel { id, server, .. } => {
+        Channel::TextChannel { id, server, .. }
+        | Channel::VoiceChannel { id, server, .. } => {
             Invite::Server {
                 code: code.clone(),
                 creator: user.id,
diff --git a/src/routes/channels/message_delete.rs b/src/routes/channels/message_delete.rs
index fa794d7..c0f3ac5 100644
--- a/src/routes/channels/message_delete.rs
+++ b/src/routes/channels/message_delete.rs
@@ -6,6 +6,7 @@ use mongodb::bson::doc;
 #[delete("/<target>/messages/<msg>")]
 pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
     let channel = target.fetch_channel().await?;
+    channel.has_messaging()?;
 
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&channel)
diff --git a/src/routes/channels/message_edit.rs b/src/routes/channels/message_edit.rs
index 19d95be..a356a68 100644
--- a/src/routes/channels/message_edit.rs
+++ b/src/routes/channels/message_edit.rs
@@ -19,6 +19,8 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<
         .map_err(|error| Error::FailedValidation { error })?;
 
     let channel = target.fetch_channel().await?;
+    channel.has_messaging()?;
+
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&channel)
         .for_channel()
diff --git a/src/routes/channels/message_fetch.rs b/src/routes/channels/message_fetch.rs
index d308c84..c87a333 100644
--- a/src/routes/channels/message_fetch.rs
+++ b/src/routes/channels/message_fetch.rs
@@ -6,6 +6,7 @@ use rocket_contrib::json::JsonValue;
 #[get("/<target>/messages/<msg>")]
 pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
     let channel = target.fetch_channel().await?;
+    channel.has_messaging()?;
 
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&channel)
diff --git a/src/routes/channels/message_query.rs b/src/routes/channels/message_query.rs
index e1d67b8..4492e83 100644
--- a/src/routes/channels/message_query.rs
+++ b/src/routes/channels/message_query.rs
@@ -38,6 +38,7 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json
         .map_err(|error| Error::FailedValidation { error })?;
 
     let target = target.fetch_channel().await?;
+    target.has_messaging()?;
 
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&target)
diff --git a/src/routes/channels/message_query_stale.rs b/src/routes/channels/message_query_stale.rs
index 2930a42..5f83081 100644
--- a/src/routes/channels/message_query_stale.rs
+++ b/src/routes/channels/message_query_stale.rs
@@ -18,6 +18,7 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal
     }
 
     let target = target.fetch_channel().await?;
+    target.has_messaging()?;
 
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&target)
diff --git a/src/routes/channels/message_send.rs b/src/routes/channels/message_send.rs
index 9bf0f9c..18259d7 100644
--- a/src/routes/channels/message_send.rs
+++ b/src/routes/channels/message_send.rs
@@ -1,3 +1,5 @@
+use std::collections::HashSet;
+
 use crate::database::*;
 use crate::util::result::{Error, Result};
 
@@ -8,6 +10,12 @@ use serde::{Deserialize, Serialize};
 use ulid::Ulid;
 use validator::Validate;
 
+#[derive(Serialize, Deserialize)]
+pub struct Reply {
+    id: String,
+    mention: bool
+}
+
 #[derive(Validate, Serialize, Deserialize)]
 pub struct Data {
     #[validate(length(min = 0, max = 2000))]
@@ -17,6 +25,7 @@ pub struct Data {
     nonce: String,
     #[validate(length(min = 1, max = 128))]
     attachments: Option<Vec<String>>,
+    replies: Option<Vec<Reply>>,
 }
 
 lazy_static! {
@@ -25,6 +34,7 @@ lazy_static! {
 
 #[post("/<target>/messages", data = "<message>")]
 pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> {
+    let message = message.into_inner();
     message
         .validate()
         .map_err(|error| Error::FailedValidation { error })?;
@@ -36,6 +46,8 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
     }
 
     let target = target.fetch_channel().await?;
+    target.has_messaging()?;
+    
     let perm = permissions::PermissionCalculator::new(&user)
         .with_channel(&target)
         .for_channel()
@@ -65,42 +77,64 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
     }
 
     let id = Ulid::new().to_string();
-    let attachments = if let Some(ids) = &message.attachments {
+
+    let mut mentions = HashSet::new();
+    if let Some(captures) = RE_ULID.captures_iter(&message.content).next() {
+        // ! FIXME: in the future, verify in group so we can send out push
+        mentions.insert(captures[1].to_string());
+    }
+
+    let mut replies = HashSet::new();
+    if let Some(entries) = message.replies {
+        // ! FIXME: move this to app config
+        if entries.len() >= 5 {
+            return Err(Error::TooManyReplies)
+        }
+
+        for Reply { id, mention } in entries {
+            let message = Ref::from_unchecked(id)
+                .fetch_message(&target)
+                .await?;
+            
+            replies.insert(message.id);
+            
+            if mention {
+                mentions.insert(message.author);
+            }
+        }
+    }
+
+    let mut attachments = vec![];
+    if let Some(ids) = &message.attachments {
         // ! FIXME: move this to app config
         if ids.len() >= 5 {
             return Err(Error::TooManyAttachments)
         }
 
-        let mut attachments = vec![];
         for attachment_id in ids {
             attachments
                 .push(File::find_and_use(attachment_id, "attachments", "message", &id).await?);
         }
-
-        Some(attachments)
-    } else {
-        None
-    };
-
-    let mut mentions = vec![];
-    if let Some(captures) = RE_ULID.captures_iter(&message.content).next() {
-        // ! FIXME: in the future, verify in group so we can send out push
-        mentions.push(captures[1].to_string());
     }
 
-
     let msg = Message {
         id,
         channel: target.id().to_string(),
         author: user.id,
 
         content: Content::Text(message.content.clone()),
-        attachments,
         nonce: Some(message.nonce.clone()),
         edited: None,
         embeds: None,
+
+        attachments: if attachments.len() > 0 { Some(attachments) } else { None },
         mentions: if mentions.len() > 0 {
-            Some(mentions)
+            Some(mentions.into_iter().collect::<Vec<String>>())
+        } else {
+            None
+        },
+        replies: if replies.len() > 0 {
+            Some(replies.into_iter().collect::<Vec<String>>())
         } else {
             None
         },
diff --git a/src/routes/servers/channel_create.rs b/src/routes/servers/channel_create.rs
index d98bec0..c96d1cd 100644
--- a/src/routes/servers/channel_create.rs
+++ b/src/routes/servers/channel_create.rs
@@ -7,8 +7,22 @@ use serde::{Deserialize, Serialize};
 use ulid::Ulid;
 use validator::Validate;
 
+#[derive(Serialize, Deserialize)]
+enum ChannelType {
+    Text,
+    Voice
+}
+
+impl Default for ChannelType {
+    fn default() -> Self {
+        ChannelType::Text
+    }
+}
+
 #[derive(Validate, Serialize, Deserialize)]
 pub struct Data {
+    #[serde(rename = "type", default = "ChannelType::default")]
+    channel_type: ChannelType,
     #[validate(length(min = 1, max = 32))]
     name: String,
     #[validate(length(min = 0, max = 1024))]
@@ -30,7 +44,7 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue>
         .for_server()
         .await?;
 
-    if !perm.get_manage_server() {
+    if !perm.get_manage_channels() {
         Err(Error::MissingPermission)?
     }
 
@@ -52,15 +66,26 @@ pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<JsonValue>
     }
 
     let id = Ulid::new().to_string();
-    let channel = Channel::TextChannel {
-        id: id.clone(),
-        server: target.id.clone(),
-        nonce: Some(info.nonce),
+    let channel = match info.channel_type {
+        ChannelType::Text => Channel::TextChannel {
+            id: id.clone(),
+            server: target.id.clone(),
+            nonce: Some(info.nonce),
+
+            name: info.name,
+            description: info.description,
+            icon: None,
+            last_message: None,
+        },
+        ChannelType::Voice => Channel::VoiceChannel {
+            id: id.clone(),
+            server: target.id.clone(),
+            nonce: Some(info.nonce),
 
-        name: info.name,
-        description: info.description,
-        icon: None,
-        last_message: None,
+            name: info.name,
+            description: info.description,
+            icon: None
+        }
     };
 
     channel.clone().publish().await?;
diff --git a/src/util/result.rs b/src/util/result.rs
index e0df975..218a536 100644
--- a/src/util/result.rs
+++ b/src/util/result.rs
@@ -26,9 +26,11 @@ pub enum Error {
     // ? Channel related errors.
     UnknownChannel,
     UnknownAttachment,
+    UnknownMessage,
     CannotEditMessage,
     CannotJoinCall,
     TooManyAttachments,
+    TooManyReplies,
     EmptyMessage,
     CannotRemoveYourself,
     GroupTooLarge {
@@ -79,10 +81,12 @@ impl<'r> Responder<'r, 'static> for Error {
             Error::NotFriends => Status::Forbidden,
 
             Error::UnknownChannel => Status::NotFound,
+            Error::UnknownMessage => Status::NotFound,
             Error::UnknownAttachment => Status::BadRequest,
             Error::CannotEditMessage => Status::Forbidden,
             Error::CannotJoinCall => Status::BadRequest,
             Error::TooManyAttachments => Status::BadRequest,
+            Error::TooManyReplies => Status::BadRequest,
             Error::EmptyMessage => Status::UnprocessableEntity,
             Error::CannotRemoveYourself => Status::BadRequest,
             Error::GroupTooLarge { .. } => Status::Forbidden,
diff --git a/src/version.rs b/src/version.rs
index 0c8c63d..0625890 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1 +1 @@
-pub const VERSION: &str = "0.5.0-alpha.5";
+pub const VERSION: &str = "0.5.1-alpha.0";
-- 
GitLab