From 4727f997edb31abed570820b0026362341c44c96 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Wed, 16 Jun 2021 23:24:31 +0100
Subject: [PATCH] New Feature: Add server-side channel unreads.

---
 set_version.sh                     |  2 +-
 src/database/entities/channel.rs   | 14 ++++++++
 src/database/entities/server.rs    |  2 ++
 src/database/entities/sync.rs      | 16 +++++++++
 src/database/entities/user.rs      | 19 +++++++++++
 src/database/migrations/init.rs    |  8 ++---
 src/notifications/events.rs        |  7 +++-
 src/notifications/payload.rs       |  2 +-
 src/notifications/websocket.rs     |  3 +-
 src/routes/channels/channel_ack.rs | 53 ++++++++++++++++++++++++++++++
 src/routes/channels/mod.rs         |  2 ++
 src/routes/sync/get_unreads.rs     | 10 ++++++
 src/routes/sync/mod.rs             |  3 +-
 src/version.rs                     |  2 +-
 14 files changed, 133 insertions(+), 10 deletions(-)
 create mode 100644 src/routes/channels/channel_ack.rs
 create mode 100644 src/routes/sync/get_unreads.rs

diff --git a/set_version.sh b/set_version.sh
index 4881d51..3b31b5d 100755
--- a/set_version.sh
+++ b/set_version.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
-export version=0.5.0-alpha.2
+export version=0.5.0-alpha.3
 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs
index 0ab74fc..aefa613 100644
--- a/src/database/entities/channel.rs
+++ b/src/database/entities/channel.rs
@@ -128,6 +128,20 @@ impl Channel {
                 operation: "delete_many",
                 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",
+            })?;
 
         // Check if there are any attachments we need to delete.
         let message_ids = messages
diff --git a/src/database/entities/server.rs b/src/database/entities/server.rs
index b30c5c4..5726d44 100644
--- a/src/database/entities/server.rs
+++ b/src/database/entities/server.rs
@@ -180,6 +180,8 @@ impl Server {
                 })?;
         }
 
+        // ! FIXME: delete any unreads
+
         for with in &["server_members", "server_bans"] {
             get_collection(with)
                 .delete_many(
diff --git a/src/database/entities/sync.rs b/src/database/entities/sync.rs
index d905c69..07fb161 100644
--- a/src/database/entities/sync.rs
+++ b/src/database/entities/sync.rs
@@ -1,3 +1,19 @@
 use std::collections::HashMap;
+use serde::{Serialize, Deserialize};
 
 pub type UserSettings = HashMap<String, (i64, String)>;
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ChannelCompositeKey {
+    pub channel: String,
+    pub user: String,
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone)]
+pub struct ChannelUnread {
+    #[serde(rename = "_id")]
+    pub id: ChannelCompositeKey,
+
+    pub last_id: String,
+    pub mentions: Option<Vec<String>>,
+}
diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs
index d205ece..b94d7a5 100644
--- a/src/database/entities/user.rs
+++ b/src/database/entities/user.rs
@@ -253,4 +253,23 @@ impl User {
             .flatten()
             .collect::<Vec<String>>())
     }
+
+    /// Utility function to fetch unread objects for user.
+    pub async fn fetch_unreads(&self) -> Result<Vec<Document>> {
+        Ok(get_collection("channel_unreads")
+            .find(
+                doc! {
+                    "_id.user": &self.id
+                },
+                None
+            )
+            .await
+            .map_err(|_| Error::DatabaseError {
+                operation: "find_one",
+                with: "user_settings",
+            })?
+            .filter_map(async move |s| s.ok())
+            .collect::<Vec<Document>>()
+            .await)
+    }
 }
diff --git a/src/database/migrations/init.rs b/src/database/migrations/init.rs
index dcb0630..1bb7277 100644
--- a/src/database/migrations/init.rs
+++ b/src/database/migrations/init.rs
@@ -41,6 +41,10 @@ pub async fn create_database() {
         .await
         .expect("Failed to create channel_invites collection.");
 
+    db.create_collection("channel_unreads", None)
+        .await
+        .expect("Failed to create channel_unreads collection.");
+
     db.create_collection("migrations", None)
         .await
         .expect("Failed to create migrations collection.");
@@ -49,10 +53,6 @@ pub async fn create_database() {
         .await
         .expect("Failed to create attachments collection.");
 
-    db.create_collection("channel_unreads", None)
-        .await
-        .expect("Failed to create channel_unreads collection.");
-
     db.create_collection("user_settings", None)
         .await
         .expect("Failed to create user_settings collection.");
diff --git a/src/notifications/events.rs b/src/notifications/events.rs
index 6ed1aaa..6218b92 100644
--- a/src/notifications/events.rs
+++ b/src/notifications/events.rs
@@ -63,7 +63,7 @@ pub enum ClientboundNotification {
     Ready {
         users: Vec<User>,
         servers: Vec<Server>,
-        channels: Vec<Channel>,
+        channels: Vec<Channel>
     },
 
     Message(Message),
@@ -103,6 +103,11 @@ pub enum ClientboundNotification {
         id: String,
         user: String,
     },
+    ChannelAck {
+        id: String,
+        user: String,
+        message_id: String
+    },
 
     ServerUpdate {
         id: String,
diff --git a/src/notifications/payload.rs b/src/notifications/payload.rs
index ac5c453..6a279f0 100644
--- a/src/notifications/payload.rs
+++ b/src/notifications/payload.rs
@@ -113,6 +113,6 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
     Ok(ClientboundNotification::Ready {
         users,
         servers,
-        channels,
+        channels
     })
 }
diff --git a/src/notifications/websocket.rs b/src/notifications/websocket.rs
index 6e275e1..5046f77 100644
--- a/src/notifications/websocket.rs
+++ b/src/notifications/websocket.rs
@@ -244,7 +244,8 @@ pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
             // Block certain notifications from reaching users that aren't meant to see them.
             match &notification {
                 ClientboundNotification::UserRelationship { id: user_id, .. }
-                | ClientboundNotification::UserSettingsUpdate { id: user_id, .. } => {
+                | ClientboundNotification::UserSettingsUpdate { id: user_id, .. }
+                | ClientboundNotification::ChannelAck { user: user_id, .. } => {
                     if &id != user_id {
                         continue;
                     }
diff --git a/src/routes/channels/channel_ack.rs b/src/routes/channels/channel_ack.rs
new file mode 100644
index 0000000..dfd45e4
--- /dev/null
+++ b/src/routes/channels/channel_ack.rs
@@ -0,0 +1,53 @@
+use crate::notifications::events::ClientboundNotification;
+use crate::util::result::{Error, Result};
+use crate::database::*;
+
+use mongodb::bson::doc;
+use mongodb::options::UpdateOptions;
+
+#[put("/<target>/ack/<message>")]
+pub async fn req(user: User, target: Ref, message: Ref) -> Result<()> {
+    let target = target.fetch_channel().await?;
+
+    let perm = permissions::PermissionCalculator::new(&user)
+        .with_channel(&target)
+        .for_channel()
+        .await?;
+
+    if !perm.get_view() {
+        Err(Error::MissingPermission)?
+    }
+
+    let id = target.id();
+    get_collection("channel_unreads")
+        .update_one(
+            doc! {
+                "_id.channel": id,
+                "_id.user": &user.id
+            },
+            doc! {
+                "$unset": {
+                    "mentions": 1
+                },
+                "$set": {
+                    "last_id": &message.id
+                }
+            },
+            UpdateOptions::builder()
+                .upsert(true)
+                .build()
+        )
+        .await
+        .map_err(|_| Error::DatabaseError {
+            operation: "update_one",
+            with: "channel_unreads",
+        })?;
+    
+    ClientboundNotification::ChannelAck {
+        id: id.to_string(),
+        user: user.id.clone(),
+        message_id: message.id
+    }.publish(user.id);
+    
+    Ok(())
+}
diff --git a/src/routes/channels/mod.rs b/src/routes/channels/mod.rs
index cb7d0b4..ad2b700 100644
--- a/src/routes/channels/mod.rs
+++ b/src/routes/channels/mod.rs
@@ -1,5 +1,6 @@
 use rocket::Route;
 
+mod channel_ack;
 mod delete_channel;
 mod edit_channel;
 mod fetch_channel;
@@ -18,6 +19,7 @@ mod message_send;
 
 pub fn routes() -> Vec<Route> {
     routes![
+        channel_ack::req,
         fetch_channel::req,
         fetch_members::req,
         delete_channel::req,
diff --git a/src/routes/sync/get_unreads.rs b/src/routes/sync/get_unreads.rs
new file mode 100644
index 0000000..4d59beb
--- /dev/null
+++ b/src/routes/sync/get_unreads.rs
@@ -0,0 +1,10 @@
+use crate::database::*;
+use crate::util::result::Result;
+
+use rocket_contrib::json::JsonValue;
+use mongodb::bson::doc;
+
+#[get("/unreads")]
+pub async fn req(user: User) -> Result<JsonValue> {
+    Ok(json!(user.fetch_unreads().await?))
+}
diff --git a/src/routes/sync/mod.rs b/src/routes/sync/mod.rs
index 872fef1..3bfe36d 100644
--- a/src/routes/sync/mod.rs
+++ b/src/routes/sync/mod.rs
@@ -2,7 +2,8 @@ use rocket::Route;
 
 mod get_settings;
 mod set_settings;
+mod get_unreads;
 
 pub fn routes() -> Vec<Route> {
-    routes![get_settings::req, set_settings::req]
+    routes![get_settings::req, set_settings::req, get_unreads::req]
 }
diff --git a/src/version.rs b/src/version.rs
index b8bf57c..133b97d 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1 +1 @@
-pub const VERSION: &str = "0.5.0-alpha.1";
+pub const VERSION: &str = "0.5.0-alpha.3";
-- 
GitLab