From 2a04c213f98cfd340fcb55b9d30a0751e796d431 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timo=20K=C3=B6sters?= <timo@koesters.xyz>
Date: Sun, 18 Dec 2022 09:44:46 +0100
Subject: [PATCH] improvement: handle restricted joins locally

---
 src/api/client_server/membership.rs | 124 +++++++++++++++++++++-------
 src/service/globals/mod.rs          |   2 +-
 2 files changed, 97 insertions(+), 29 deletions(-)

diff --git a/src/api/client_server/membership.rs b/src/api/client_server/membership.rs
index 8674a6053..d6a1bd8ca 100644
--- a/src/api/client_server/membership.rs
+++ b/src/api/client_server/membership.rs
@@ -13,8 +13,9 @@
     canonical_json::to_canonical_value,
     events::{
         room::{
-            join_rules::{JoinRule, RoomJoinRulesEventContent},
+            join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
             member::{MembershipState, RoomMemberEventContent},
+            power_levels::RoomPowerLevelsEventContent,
         },
         RoomEventType, StateEventType,
     },
@@ -751,6 +752,96 @@ async fn join_room_by_id_helper(
             .state
             .set_room_state(room_id, statehash_after_join, &state_lock)?;
     } else {
+        let join_rules_event = services().rooms.state_accessor.room_state_get(
+            &room_id,
+            &StateEventType::RoomJoinRules,
+            "",
+        )?;
+        let power_levels_event = services().rooms.state_accessor.room_state_get(
+            &room_id,
+            &StateEventType::RoomPowerLevels,
+            "",
+        )?;
+
+        let join_rules_event_content: Option<RoomJoinRulesEventContent> = join_rules_event
+            .as_ref()
+            .map(|join_rules_event| {
+                serde_json::from_str(join_rules_event.content.get()).map_err(|e| {
+                    warn!("Invalid join rules event: {}", e);
+                    Error::bad_database("Invalid join rules event in db.")
+                })
+            })
+            .transpose()?;
+        let power_levels_event_content: Option<RoomPowerLevelsEventContent> = power_levels_event
+            .as_ref()
+            .map(|power_levels_event| {
+                serde_json::from_str(power_levels_event.content.get()).map_err(|e| {
+                    warn!("Invalid power levels event: {}", e);
+                    Error::bad_database("Invalid power levels event in db.")
+                })
+            })
+            .transpose()?;
+
+        let restriction_rooms = match join_rules_event_content {
+            Some(RoomJoinRulesEventContent {
+                join_rule: JoinRule::Restricted(restricted),
+            })
+            | Some(RoomJoinRulesEventContent {
+                join_rule: JoinRule::KnockRestricted(restricted),
+            }) => restricted
+                .allow
+                .into_iter()
+                .filter_map(|a| match a {
+                    AllowRule::RoomMembership(r) => Some(r.room_id),
+                    _ => None,
+                })
+                .collect(),
+            _ => Vec::new(),
+        };
+
+        let authorized_user = restriction_rooms
+            .iter()
+            .find_map(|restriction_room_id| {
+                if !services()
+                    .rooms
+                    .state_cache
+                    .is_joined(sender_user, restriction_room_id)
+                    .ok()?
+                {
+                    return None;
+                }
+                let authorized_user = power_levels_event_content
+                    .as_ref()
+                    .and_then(|c| {
+                        c.users
+                            .iter()
+                            .filter(|(uid, i)| {
+                                uid.server_name() == services().globals.server_name()
+                                    && **i > ruma::int!(0)
+                                    && services()
+                                        .rooms
+                                        .state_cache
+                                        .is_joined(uid, restriction_room_id)
+                                        .unwrap_or(false)
+                            })
+                            .max_by_key(|(_, i)| *i)
+                            .map(|(u, _)| u.to_owned())
+                    })
+                    .or_else(|| {
+                        // TODO: Check here if user is actually allowed to invite. Currently the auth
+                        // check will just fail in this case.
+                        services()
+                            .rooms
+                            .state_cache
+                            .room_members(restriction_room_id)
+                            .filter_map(|r| r.ok())
+                            .filter(|uid| uid.server_name() == services().globals.server_name())
+                            .next()
+                    });
+                Some(authorized_user)
+            })
+            .flatten();
+
         let event = RoomMemberEventContent {
             membership: MembershipState::Join,
             displayname: services().users.displayname(sender_user)?,
@@ -759,7 +850,7 @@ async fn join_room_by_id_helper(
             third_party_invite: None,
             blurhash: services().users.blurhash(sender_user)?,
             reason: None,
-            join_authorized_via_users_server: None,
+            join_authorized_via_users_server: authorized_user,
         };
 
         // Try normal join first
@@ -779,32 +870,9 @@ async fn join_room_by_id_helper(
             Err(e) => e,
         };
 
-        // TODO: Conduit does not implement restricted join rules yet, we always ask over
-        // federation
-        let join_rules_event = services().rooms.state_accessor.room_state_get(
-            &room_id,
-            &StateEventType::RoomJoinRules,
-            "",
-        )?;
-
-        let join_rules_event_content: Option<RoomJoinRulesEventContent> = join_rules_event
-            .as_ref()
-            .map(|join_rules_event| {
-                serde_json::from_str(join_rules_event.content.get()).map_err(|e| {
-                    warn!("Invalid join rules event: {}", e);
-                    Error::bad_database("Invalid join rules event in db.")
-                })
-            })
-            .transpose()?;
-
-        if matches!(
-            join_rules_event_content,
-            Some(RoomJoinRulesEventContent {
-                join_rule: JoinRule::Restricted { .. }
-            }) | Some(RoomJoinRulesEventContent {
-                join_rule: JoinRule::KnockRestricted { .. }
-            })
-        ) {
+        if !restriction_rooms.is_empty() {
+            // We couldn't do the join locally, maybe federation can help to satisfy the restricted
+            // join requirements
             let (make_join_response, remote_server) =
                 make_join_request(sender_user, room_id, servers).await?;
 
diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs
index affc05166..c0fcb4bdc 100644
--- a/src/service/globals/mod.rs
+++ b/src/service/globals/mod.rs
@@ -168,7 +168,7 @@ pub fn load(db: &'static dyn Data, config: Config) -> Result<Self> {
             .supported_room_versions()
             .contains(&s.config.default_room_version)
         {
-            error!("Room version in config isn't supported, falling back to default version");
+            error!(config=?s.config.default_room_version, fallback=?crate::config::default_default_room_version(), "Room version in config isn't supported, falling back to default version");
             s.config.default_room_version = crate::config::default_default_room_version();
         };
 
-- 
GitLab