From ae76052378d99b6f49d8a7c54699c4ac8fdc6846 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Kub=C3=ADk?= <jakub.kubik.it@protonmail.com>
Date: Fri, 8 Sep 2023 14:36:39 +0200
Subject: [PATCH] feat(presence): add granular allow configuration

---
 src/api/client_server/presence.rs             | 14 +++
 src/api/client_server/profile.rs              | 28 +++---
 src/api/client_server/sync.rs                 | 91 +++++++++++--------
 src/api/server_server.rs                      |  4 +
 src/config/mod.rs                             |  6 +-
 src/database/key_value/rooms/edus/presence.rs | 18 +---
 src/database/mod.rs                           |  2 +-
 src/service/globals/mod.rs                    | 12 ++-
 src/service/sending/mod.rs                    | 56 ++++++------
 9 files changed, 133 insertions(+), 98 deletions(-)

diff --git a/src/api/client_server/presence.rs b/src/api/client_server/presence.rs
index 46cad8fe..3d56e6f3 100644
--- a/src/api/client_server/presence.rs
+++ b/src/api/client_server/presence.rs
@@ -11,6 +11,13 @@ use std::time::Duration;
 pub async fn set_presence_route(
     body: Ruma<set_presence::v3::Request>,
 ) -> Result<set_presence::v3::Response> {
+    if !services().globals.allow_local_presence() {
+        return Err(Error::BadRequest(
+            ErrorKind::Forbidden,
+            "Presence is disabled on this server",
+        ));
+    }
+
     let sender_user = body.sender_user.as_ref().expect("user is authenticated");
     for room_id in services().rooms.state_cache.rooms_joined(sender_user) {
         let room_id = room_id?;
@@ -36,6 +43,13 @@ pub async fn set_presence_route(
 pub async fn get_presence_route(
     body: Ruma<get_presence::v3::Request>,
 ) -> Result<get_presence::v3::Response> {
+    if !services().globals.allow_local_presence() {
+        return Err(Error::BadRequest(
+            ErrorKind::Forbidden,
+            "Presence is disabled on this server",
+        ));
+    }
+
     let sender_user = body.sender_user.as_ref().expect("user is authenticated");
 
     let mut presence_event = None;
diff --git a/src/api/client_server/profile.rs b/src/api/client_server/profile.rs
index 00d52eb0..07355301 100644
--- a/src/api/client_server/profile.rs
+++ b/src/api/client_server/profile.rs
@@ -92,12 +92,14 @@ pub async fn set_displayname_route(
             .await;
     }
 
-    // Presence update
-    services()
-        .rooms
-        .edus
-        .presence
-        .ping_presence(sender_user, PresenceState::Online)?;
+    if services().globals.allow_local_presence() {
+        // Presence update
+        services()
+            .rooms
+            .edus
+            .presence
+            .ping_presence(sender_user, PresenceState::Online)?;
+    }
 
     Ok(set_display_name::v3::Response {})
 }
@@ -213,12 +215,14 @@ pub async fn set_avatar_url_route(
             .await;
     }
 
-    // Presence update
-    services()
-        .rooms
-        .edus
-        .presence
-        .ping_presence(sender_user, PresenceState::Online)?;
+    if services().globals.allow_local_presence() {
+        // Presence update
+        services()
+            .rooms
+            .edus
+            .presence
+            .ping_presence(sender_user, PresenceState::Online)?;
+    }
 
     Ok(set_avatar_url::v3::Response {})
 }
diff --git a/src/api/client_server/sync.rs b/src/api/client_server/sync.rs
index 7b7224c3..ac8dcd5d 100644
--- a/src/api/client_server/sync.rs
+++ b/src/api/client_server/sync.rs
@@ -18,6 +18,7 @@ use ruma::{
         uiaa::UiaaResponse,
     },
     events::{
+        presence::PresenceEvent,
         room::member::{MembershipState, RoomMemberEventContent},
         StateEventType, TimelineEventType,
     },
@@ -175,11 +176,13 @@ async fn sync_helper(
     // bool = caching allowed
 ) -> Result<(sync_events::v3::Response, bool), Error> {
     // Presence update
-    services()
-        .rooms
-        .edus
-        .presence
-        .ping_presence(&sender_user, body.set_presence)?;
+    if services().globals.allow_local_presence() {
+        services()
+            .rooms
+            .edus
+            .presence
+            .ping_presence(&sender_user, body.set_presence)?;
+    }
 
     // Setup watchers, so if there's no response, we can wait for them
     let watcher = services().globals.watch(&sender_user, &sender_device);
@@ -255,39 +258,8 @@ async fn sync_helper(
                 joined_rooms.insert(room_id.clone(), joined_room);
             }
 
-            // Take presence updates from this room
-            for presence_data in services()
-                .rooms
-                .edus
-                .presence
-                .presence_since(&room_id, since)
-            {
-                let (user_id, _, presence_event) = presence_data?;
-
-                match presence_updates.entry(user_id) {
-                    Entry::Vacant(slot) => {
-                        slot.insert(presence_event);
-                    }
-                    Entry::Occupied(mut slot) => {
-                        let curr_event = slot.get_mut();
-                        let curr_content = &mut curr_event.content;
-                        let new_content = presence_event.content;
-
-                        // Update existing presence event with more info
-                        curr_content.presence = new_content.presence;
-                        curr_content.status_msg =
-                            curr_content.status_msg.clone().or(new_content.status_msg);
-                        curr_content.last_active_ago =
-                            curr_content.last_active_ago.or(new_content.last_active_ago);
-                        curr_content.displayname =
-                            curr_content.displayname.clone().or(new_content.displayname);
-                        curr_content.avatar_url =
-                            curr_content.avatar_url.clone().or(new_content.avatar_url);
-                        curr_content.currently_active = curr_content
-                            .currently_active
-                            .or(new_content.currently_active);
-                    }
-                }
+            if services().globals.allow_local_presence() {
+                process_room_presence_updates(&mut presence_updates, &room_id, since).await?;
             }
         }
     }
@@ -599,6 +571,49 @@ async fn sync_helper(
     }
 }
 
+async fn process_room_presence_updates(
+    presence_updates: &mut HashMap<OwnedUserId, PresenceEvent>,
+    room_id: &RoomId,
+    since: u64,
+) -> Result<()> {
+    // Take presence updates from this room
+    for presence_data in services()
+        .rooms
+        .edus
+        .presence
+        .presence_since(room_id, since)
+    {
+        let (user_id, _, presence_event) = presence_data?;
+
+        match presence_updates.entry(user_id) {
+            Entry::Vacant(slot) => {
+                slot.insert(presence_event);
+            }
+            Entry::Occupied(mut slot) => {
+                let curr_event = slot.get_mut();
+                let curr_content = &mut curr_event.content;
+                let new_content = presence_event.content;
+
+                // Update existing presence event with more info
+                curr_content.presence = new_content.presence;
+                curr_content.status_msg =
+                    curr_content.status_msg.clone().or(new_content.status_msg);
+                curr_content.last_active_ago =
+                    curr_content.last_active_ago.or(new_content.last_active_ago);
+                curr_content.displayname =
+                    curr_content.displayname.clone().or(new_content.displayname);
+                curr_content.avatar_url =
+                    curr_content.avatar_url.clone().or(new_content.avatar_url);
+                curr_content.currently_active = curr_content
+                    .currently_active
+                    .or(new_content.currently_active);
+            }
+        }
+    }
+
+    Ok(())
+}
+
 #[allow(clippy::too_many_arguments)]
 async fn load_joined_room(
     sender_user: &UserId,
diff --git a/src/api/server_server.rs b/src/api/server_server.rs
index 178d0c99..59e656fe 100644
--- a/src/api/server_server.rs
+++ b/src/api/server_server.rs
@@ -778,6 +778,10 @@ pub async fn send_transaction_message_route(
     {
         match edu {
             Edu::Presence(presence) => {
+                if !services().globals.allow_incoming_presence() {
+                    continue;
+                }
+
                 for update in presence.push {
                     for room_id in services().rooms.state_cache.rooms_joined(&update.user_id) {
                         services().rooms.edus.presence.set_presence(
diff --git a/src/config/mod.rs b/src/config/mod.rs
index bef5ebf8..16930fb0 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -84,7 +84,11 @@ pub struct Config {
     pub emergency_password: Option<String>,
 
     #[serde(default = "false_fn")]
-    pub allow_presence: bool,
+    pub allow_local_presence: bool,
+    #[serde(default = "false_fn")]
+    pub allow_incoming_presence: bool,
+    #[serde(default = "false_fn")]
+    pub allow_outgoing_presence: bool,
     #[serde(default = "default_presence_idle_timeout_s")]
     pub presence_idle_timeout_s: u64,
     #[serde(default = "default_presence_offline_timeout_s")]
diff --git a/src/database/key_value/rooms/edus/presence.rs b/src/database/key_value/rooms/edus/presence.rs
index a2c8ee44..d70117e0 100644
--- a/src/database/key_value/rooms/edus/presence.rs
+++ b/src/database/key_value/rooms/edus/presence.rs
@@ -1,4 +1,4 @@
-use std::{iter, time::Duration};
+use std::time::Duration;
 
 use ruma::{
     events::presence::PresenceEvent, presence::PresenceState, OwnedUserId, RoomId, UInt, UserId,
@@ -14,10 +14,6 @@ use crate::{
 
 impl service::rooms::edus::presence::Data for KeyValueDatabase {
     fn get_presence(&self, room_id: &RoomId, user_id: &UserId) -> Result<Option<PresenceEvent>> {
-        if !services().globals.config.allow_presence {
-            return Ok(None);
-        }
-
         let key = presence_key(room_id, user_id);
 
         self.roomuserid_presence
@@ -29,10 +25,6 @@ impl service::rooms::edus::presence::Data for KeyValueDatabase {
     }
 
     fn ping_presence(&self, user_id: &UserId, new_state: PresenceState) -> Result<()> {
-        if !services().globals.config.allow_presence {
-            return Ok(());
-        }
-
         let now = utils::millis_since_unix_epoch();
         let mut state_changed = false;
 
@@ -103,10 +95,6 @@ impl service::rooms::edus::presence::Data for KeyValueDatabase {
         last_active_ago: Option<UInt>,
         status_msg: Option<String>,
     ) -> Result<()> {
-        if !services().globals.config.allow_presence {
-            return Ok(());
-        }
-
         let now = utils::millis_since_unix_epoch();
         let last_active_ts = match last_active_ago {
             Some(last_active_ago) => now.saturating_sub(last_active_ago.into()),
@@ -153,10 +141,6 @@ impl service::rooms::edus::presence::Data for KeyValueDatabase {
         room_id: &RoomId,
         since: u64,
     ) -> Box<dyn Iterator<Item = Result<(OwnedUserId, u64, PresenceEvent)>> + 'a> {
-        if !services().globals.config.allow_presence {
-            return Box::new(iter::empty());
-        }
-
         let prefix = [room_id.as_bytes(), &[0xff]].concat();
 
         Box::new(
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 7d7dad7c..fb07c205 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -988,7 +988,7 @@ impl KeyValueDatabase {
         if services().globals.allow_check_for_updates() {
             Self::start_check_for_updates_task();
         }
-        if services().globals.config.allow_presence {
+        if services().globals.allow_local_presence() {
             Self::start_presence_handler(presence_receiver).await;
         }
 
diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs
index 1f4fc382..d3185130 100644
--- a/src/service/globals/mod.rs
+++ b/src/service/globals/mod.rs
@@ -353,8 +353,16 @@ impl Service {
         &self.config.emergency_password
     }
 
-    pub fn allow_presence(&self) -> bool {
-        self.config.allow_presence
+    pub fn allow_local_presence(&self) -> bool {
+        self.config.allow_local_presence
+    }
+
+    pub fn allow_incoming_presence(&self) -> bool {
+        self.config.allow_incoming_presence
+    }
+
+    pub fn allow_outcoming_presence(&self) -> bool {
+        self.config.allow_outgoing_presence
     }
 
     pub fn presence_idle_timeout_s(&self) -> u64 {
diff --git a/src/service/sending/mod.rs b/src/service/sending/mod.rs
index eb8fe849..670463a7 100644
--- a/src/service/sending/mod.rs
+++ b/src/service/sending/mod.rs
@@ -286,39 +286,41 @@ impl Service {
                     .filter(|user_id| user_id.server_name() == services().globals.server_name()),
             );
 
-            // Look for presence updates in this room
-            let mut presence_updates = Vec::new();
+            if services().globals.allow_outcoming_presence() {
+                // Look for presence updates in this room
+                let mut presence_updates = Vec::new();
 
-            for presence_data in services()
-                .rooms
-                .edus
-                .presence
-                .presence_since(&room_id, since)
-            {
-                let (user_id, count, presence_event) = presence_data?;
+                for presence_data in services()
+                    .rooms
+                    .edus
+                    .presence
+                    .presence_since(&room_id, since)
+                {
+                    let (user_id, count, presence_event) = presence_data?;
 
-                if count > max_edu_count {
-                    max_edu_count = count;
+                    if count > max_edu_count {
+                        max_edu_count = count;
+                    }
+
+                    if user_id.server_name() != services().globals.server_name() {
+                        continue;
+                    }
+
+                    presence_updates.push(PresenceUpdate {
+                        user_id,
+                        presence: presence_event.content.presence,
+                        currently_active: presence_event.content.currently_active.unwrap_or(false),
+                        last_active_ago: presence_event.content.last_active_ago.unwrap_or(uint!(0)),
+                        status_msg: presence_event.content.status_msg,
+                    });
                 }
 
-                if user_id.server_name() != services().globals.server_name() {
-                    continue;
-                }
-
-                presence_updates.push(PresenceUpdate {
-                    user_id,
-                    presence: presence_event.content.presence,
-                    currently_active: presence_event.content.currently_active.unwrap_or(false),
-                    last_active_ago: presence_event.content.last_active_ago.unwrap_or(uint!(0)),
-                    status_msg: presence_event.content.status_msg,
-                });
+                let presence_content = Edu::Presence(PresenceContent::new(presence_updates));
+                events.push(
+                    serde_json::to_vec(&presence_content).expect("PresenceEvent can be serialized"),
+                );
             }
 
-            let presence_content = Edu::Presence(PresenceContent::new(presence_updates));
-            events.push(
-                serde_json::to_vec(&presence_content).expect("PresenceEvent can be serialized"),
-            );
-
             // Look for read receipts in this room
             for r in services()
                 .rooms