From b5e318561c1fe9f3b43ddb8fcfc220abceb677d5 Mon Sep 17 00:00:00 2001 From: Matthias Ahouansou Date: Thu, 3 Jul 2025 12:41:16 +0100 Subject: [PATCH] feat: MSC4289, Explicitly privilege room creators (1/2) --- Cargo.lock | 22 +-- src/api/client_server/room.rs | 147 +++++++++++++------ src/service/admin/mod.rs | 18 ++- src/service/pusher/mod.rs | 23 +-- src/service/rooms/alias/mod.rs | 33 ++--- src/service/rooms/state_accessor/mod.rs | 88 +++++++----- src/service/rooms/timeline/mod.rs | 178 ++++++++++++------------ 7 files changed, 282 insertions(+), 227 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c426f8f3..ce186c1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2521,7 +2521,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.5" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "assign", "js_int", @@ -2540,7 +2540,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.12.2" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "js_int", "ruma-common", @@ -2552,7 +2552,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "as_variant", "assign", @@ -2575,7 +2575,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "as_variant", "base64 0.22.1", @@ -2607,7 +2607,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.4" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "as_variant", "indexmap 2.9.0", @@ -2631,7 +2631,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "bytes", "headers", @@ -2653,7 +2653,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "js_int", "thiserror 2.0.12", @@ -2662,7 +2662,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "cfg-if", "proc-macro-crate", @@ -2677,7 +2677,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" version = "0.11.0" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "js_int", "ruma-common", @@ -2689,7 +2689,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.17.1" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "base64 0.22.1", "ed25519-dalek", @@ -2705,7 +2705,7 @@ dependencies = [ [[package]] name = "ruma-state-res" version = "0.13.0" -source = "git+https://github.com/ruma/ruma.git#7bae3d0c0b8edf008899ac2b04cf9722182eef68" +source = "git+https://github.com/ruma/ruma.git#71069c43650d2a797181aaa2ecc63037eaece907" dependencies = [ "js_int", "ruma-common", diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs index 0ab45324..c6dcb4c3 100644 --- a/src/api/client_server/room.rs +++ b/src/api/client_server/room.rs @@ -23,11 +23,16 @@ use ruma::{ }, int, serde::JsonObject, - CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId, + CanonicalJsonObject, CanonicalJsonValue, OwnedRoomAliasId, OwnedUserId, RoomAliasId, RoomId, }; +use serde::Deserialize; use serde_json::{json, value::to_raw_value}; -use std::{cmp::max, collections::BTreeMap, sync::Arc}; -use tracing::{info, warn}; +use std::{ + cmp::max, + collections::{BTreeMap, HashSet}, + sync::Arc, +}; +use tracing::{error, info, warn}; /// # `POST /_matrix/client/r0/createRoom` /// @@ -142,9 +147,33 @@ pub async fn create_room_route( .expect("Supported room version must have rules.") .authorization; + let mut users = BTreeMap::new(); + if !rules.explicitly_privilege_room_creators { + users.insert(sender_user.clone(), int!(100)); + } + + // Figure out preset. We need it for preset specific events + let preset = body.preset.clone().unwrap_or(match &body.visibility { + room::Visibility::Private => RoomPreset::PrivateChat, + room::Visibility::Public => RoomPreset::PublicChat, + _ => RoomPreset::PrivateChat, // Room visibility should not be custom + }); + + let mut additional_creators: HashSet = HashSet::new(); + + if preset == RoomPreset::TrustedPrivateChat { + if rules.additional_room_creators { + additional_creators.extend(body.invite.clone()) + } else { + for invited_user in &body.invite { + users.insert(invited_user.clone(), int!(100)); + } + } + } + let content = match &body.creation_content { - Some(content) => { - let mut content = content + Some(raw_content) => { + let mut content = raw_content .deserialize_as_unchecked::() .expect("Invalid creation content"); @@ -157,6 +186,27 @@ pub async fn create_room_route( ); } + if rules.additional_room_creators && !additional_creators.is_empty() { + #[derive(Deserialize)] + struct AdditionalCreators { + additional_creators: Vec, + } + + if let Ok(AdditionalCreators { + additional_creators: ac, + }) = raw_content.deserialize_as_unchecked() + { + additional_creators.extend(ac); + } + + content.insert( + "additional_creators".into(), + json!(&additional_creators).try_into().map_err(|_| { + Error::BadRequest(ErrorKind::BadJson, "Invalid additional creators") + })?, + ); + } + content.insert( "room_version".into(), json!(room_version.as_str()).try_into().map_err(|_| { @@ -166,24 +216,22 @@ pub async fn create_room_route( content } None => { - let content = if rules.use_room_create_sender { - RoomCreateEventContent::new_v11() - } else { - RoomCreateEventContent::new_v1(sender_user.clone()) + let content = RoomCreateEventContent { + additional_creators: additional_creators.into_iter().collect(), + room_version, + ..if rules.use_room_create_sender { + RoomCreateEventContent::new_v11() + } else { + RoomCreateEventContent::new_v1(sender_user.clone()) + } }; - let mut content = serde_json::from_str::( + + serde_json::from_str::( to_raw_value(&content) .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid creation content"))? .get(), ) - .unwrap(); - content.insert( - "room_version".into(), - json!(room_version.as_str()).try_into().map_err(|_| { - Error::BadRequest(ErrorKind::BadJson, "Invalid creation content") - })?, - ); - content + .expect("room create event content created by us is valid") } }; @@ -250,26 +298,9 @@ pub async fn create_room_route( .await?; // 3. Power levels - - // Figure out preset. We need it for preset specific events - let preset = body.preset.clone().unwrap_or(match &body.visibility { - room::Visibility::Private => RoomPreset::PrivateChat, - room::Visibility::Public => RoomPreset::PublicChat, - _ => RoomPreset::PrivateChat, // Room visibility should not be custom - }); - - let mut users = BTreeMap::new(); - users.insert(sender_user.clone(), int!(100)); - - if preset == RoomPreset::TrustedPrivateChat { - for invite_ in &body.invite { - users.insert(invite_.clone(), int!(100)); - } - } - let mut power_levels_content = serde_json::to_value(RoomPowerLevelsEventContent { users, - ..Default::default() + ..RoomPowerLevelsEventContent::new(&rules) }) .expect("event is valid, we just created it"); @@ -587,7 +618,8 @@ pub async fn upgrade_room_route( let rules = body .new_version .rules() - .expect("Supported room version must have rules."); + .expect("Supported room version must have rules.") + .authorization; // Create a replacement room let replacement_room = RoomId::new(services().globals.server_name()); @@ -663,7 +695,7 @@ pub async fn upgrade_room_route( )); // Send a m.room.create event containing a predecessor field and the applicable room_version - if rules.authorization.use_room_create_sender { + if rules.use_room_create_sender { create_event_content.remove("creator"); } else { create_event_content.insert( @@ -674,6 +706,18 @@ pub async fn upgrade_room_route( ); } + if rules.additional_room_creators && !body.additional_creators.is_empty() { + create_event_content.insert( + "additional_creators".into(), + json!(&body.additional_creators).try_into().map_err(|_| { + Error::BadRequest( + ErrorKind::BadJson, + "Failed to convert provided additional additional creators to JSON", + ) + })?, + ); + } + create_event_content.insert( "room_version".into(), json!(&body.new_version) @@ -764,7 +808,7 @@ pub async fn upgrade_room_route( // Replicate transferable state events to the new room for event_type in transferable_state_events { - let event_content = + let mut event_content = match services() .rooms .state_accessor @@ -774,6 +818,31 @@ pub async fn upgrade_room_route( None => continue, // Skipping missing events. }; + if event_type == StateEventType::RoomPowerLevels && rules.explicitly_privilege_room_creators + { + let mut pl_event_content: CanonicalJsonObject = + serde_json::from_str(event_content.get()).map_err(|e| { + error!( + "Invalid m.room.power_levels event content in room {}: {e}", + body.room_id + ); + Error::BadDatabase("Invalid m.room.power_levels event content in room") + })?; + + if let Some(CanonicalJsonValue::Object(users)) = pl_event_content.get_mut("users") { + users.remove(sender_user.as_str()); + + if rules.additional_room_creators { + for user in &body.additional_creators { + users.remove(user.as_str()); + } + } + } + + event_content = to_raw_value(&pl_event_content) + .expect("Must serialize, only changes made was removing keys") + } + services() .rooms .timeline diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs index 1aa5af3d..92f09acd 100644 --- a/src/service/admin/mod.rs +++ b/src/service/admin/mod.rs @@ -1756,7 +1756,9 @@ impl Service { // 3. Power levels let mut users = BTreeMap::new(); - users.insert(conduit_user.to_owned(), 100.into()); + if !rules.explicitly_privilege_room_creators { + users.insert(conduit_user.to_owned(), 100.into()); + } services() .rooms @@ -1766,7 +1768,7 @@ impl Service { event_type: TimelineEventType::RoomPowerLevels, content: to_raw_value(&RoomPowerLevelsEventContent { users, - ..Default::default() + ..RoomPowerLevelsEventContent::new(&rules) }) .expect("event is valid, we just created it"), unsigned: None, @@ -2010,8 +2012,16 @@ impl Service { .await?; // Set power level + let room_version = services().rooms.state.get_room_version(&room_id)?; + let rules = room_version + .rules() + .expect("Supported room version must have rules.") + .authorization; + let mut users = BTreeMap::new(); - users.insert(conduit_user.to_owned(), 100.into()); + if !rules.explicitly_privilege_room_creators { + users.insert(conduit_user.to_owned(), 100.into()); + } users.insert(user_id.to_owned(), 100.into()); services() @@ -2022,7 +2032,7 @@ impl Service { event_type: TimelineEventType::RoomPowerLevels, content: to_raw_value(&RoomPowerLevelsEventContent { users, - ..Default::default() + ..RoomPowerLevelsEventContent::new(&rules) }) .expect("event is valid, we just created it"), unsigned: None, diff --git a/src/service/pusher/mod.rs b/src/service/pusher/mod.rs index 89e3f518..7fc60599 100644 --- a/src/service/pusher/mod.rs +++ b/src/service/pusher/mod.rs @@ -13,7 +13,7 @@ use ruma::{ }, IncomingResponse, OutgoingRequest, SendAccessToken, }, - events::{room::power_levels::RoomPowerLevelsEventContent, StateEventType, TimelineEventType}, + events::TimelineEventType, push::{Action, PushConditionRoomCtx, PushFormat, Ruleset, Tweak}, serde::Raw, uint, RoomId, UInt, UserId, @@ -139,21 +139,12 @@ impl Service { let mut notify = None; let mut tweaks = Vec::new(); - let power_levels: RoomPowerLevelsEventContent = services() - .rooms - .state_accessor - .room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")? - .map(|ev| { - serde_json::from_str(ev.content.get()) - .map_err(|_| Error::bad_database("invalid m.room.power_levels event")) - }) - .transpose()? - .unwrap_or_default(); + let power_levels = services().rooms.state_accessor.power_levels(&pdu.room_id)?; for action in self.get_actions( user, &ruleset, - &power_levels, + power_levels.into(), &pdu.to_sync_room_event(), &pdu.room_id, )? { @@ -188,16 +179,10 @@ impl Service { &self, user: &UserId, ruleset: &'a Ruleset, - power_levels: &RoomPowerLevelsEventContent, + power_levels: PushConditionPowerLevelsCtx, pdu: &Raw, room_id: &RoomId, ) -> Result<&'a [Action]> { - let power_levels = PushConditionPowerLevelsCtx { - users: power_levels.users.clone(), - users_default: power_levels.users_default, - notifications: power_levels.notifications.clone(), - }; - let ctx = PushConditionRoomCtx { room_id: room_id.to_owned(), member_count: 10_u32.into(), // TODO: get member count efficiently diff --git a/src/service/rooms/alias/mod.rs b/src/service/rooms/alias/mod.rs index e27cfc6f..d2a30150 100644 --- a/src/service/rooms/alias/mod.rs +++ b/src/service/rooms/alias/mod.rs @@ -2,7 +2,6 @@ mod data; pub use data::Data; use rand::seq::SliceRandom; -use tracing::error; use crate::{services, Error, Result}; use ruma::{ @@ -11,10 +10,7 @@ use ruma::{ client::{alias::get_alias, error::ErrorKind}, federation, }, - events::{ - room::power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, - StateEventType, - }, + events::StateEventType, OwnedRoomAliasId, OwnedRoomId, RoomAliasId, RoomId, UserId, }; @@ -55,27 +51,14 @@ impl Service { { Ok(true) // Checking whether the user is able to change canonical aliases of the room - } else if let Some(event) = services().rooms.state_accessor.room_state_get( - &room_id, - &StateEventType::RoomPowerLevels, - "", - )? { - serde_json::from_str(event.content.get()) - .map_err(|_| Error::bad_database("Invalid event content for m.room.power_levels")) - .map(|content: RoomPowerLevelsEventContent| { - RoomPowerLevels::from(content) - .user_can_send_state(user_id, StateEventType::RoomCanonicalAlias) - }) - // If there is no power levels event, only the room creator can change canonical aliases - } else if let Some(event) = services().rooms.state_accessor.room_state_get( - &room_id, - &StateEventType::RoomCreate, - "", - )? { - Ok(event.sender == user_id) } else { - error!("Room {} has no m.room.create event (VERY BAD)!", room_id); - Err(Error::bad_database("Room has no m.room.create event")) + services() + .rooms + .state_accessor + .power_levels(&room_id) + .map(|power_levels| { + power_levels.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias) + }) } } diff --git a/src/service/rooms/state_accessor/mod.rs b/src/service/rooms/state_accessor/mod.rs index 55e0cc9d..1faf3dc5 100644 --- a/src/service/rooms/state_accessor/mod.rs +++ b/src/service/rooms/state_accessor/mod.rs @@ -20,7 +20,7 @@ use ruma::{ StateEventType, }, room::{JoinRuleSummary, RoomMembership}, - state_res::Event, + state_res::{events::RoomCreateEvent, Event}, EventId, JsOption, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId, }; use serde_json::value::to_raw_value; @@ -361,43 +361,19 @@ impl Service { room_id: &RoomId, federation: bool, ) -> Result { - self.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")? - .map(|e| { - serde_json::from_str(e.content.get()) - .map(|c: RoomPowerLevelsEventContent| c.into()) - .map(|e: RoomPowerLevels| { - e.user_can_redact_event_of_other(sender) - || e.user_can_redact_own_event(sender) - && if let Ok(Some(pdu)) = services().rooms.timeline.get_pdu(redacts) - { - if federation { - pdu.sender().server_name() == sender.server_name() - } else { - pdu.sender == sender - } - } else { - false - } - }) - .map_err(|_| { - Error::bad_database("Invalid m.room.power_levels event in database") - }) - }) - // Falling back on m.room.create to judge power levels - .unwrap_or_else(|| { - if let Some(pdu) = self.room_state_get(room_id, &StateEventType::RoomCreate, "")? { - Ok(pdu.sender == sender - || if let Ok(Some(pdu)) = services().rooms.timeline.get_pdu(redacts) { - pdu.sender == sender + self.power_levels(room_id).map(|power_levels| { + power_levels.user_can_redact_event_of_other(sender) + || power_levels.user_can_redact_own_event(sender) + && if let Ok(Some(pdu)) = services().rooms.timeline.get_pdu(redacts) { + if federation { + pdu.sender().server_name() == sender.server_name() } else { - false - }) - } else { - Err(Error::bad_database( - "No m.room.power_levels or m.room.create events in database for room", - )) - } - }) + pdu.sender == sender + } + } else { + false + } + }) } /// Checks if guests are able to join a given room @@ -485,4 +461,42 @@ impl Service { } room_ids } + + /// Gets the effective power levels of a room, regardless of if there is an + /// `m.rooms.power_levels` state. + pub fn power_levels(&self, room_id: &RoomId) -> Result { + let power_levels: Option = self + .room_state_get(room_id, &StateEventType::RoomPowerLevels, "")? + .map(|ev| { + serde_json::from_str(ev.content.get()) + .map_err(|_| Error::bad_database("invalid m.room.power_levels event.")) + }) + .transpose()?; + + let room_create = self + .room_state_get(room_id, &StateEventType::RoomCreate, "")? + .map(RoomCreateEvent::new) + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?; + + let room_version = room_create.room_version().map_err(|e| { + error!("Invalid room version in room create content for room {room_id}: {e}"); + Error::BadDatabase("Room Create content had invalid room version") + })?; + let rules = room_version + .rules() + .expect("Supported room version must have rules.") + .authorization; + + room_create + // NOTE: This is because project hydra's client-side was made public before the server + // side. This will be fixed by using `creators` instead in part 2/2. + .creator(&rules) + .map_err(|e| { + error!("Failed to get creators of room id {}: {e}", room_id); + Error::BadDatabase("RoomCreateEvent has invalid creators") + }) + .map(|creator| { + RoomPowerLevels::new(power_levels.into(), &rules, [creator.into_owned()]) + }) + } } diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 5a4dc3f7..7c64822c 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -15,12 +15,11 @@ use ruma::{ push_rules::PushRulesEvent, room::{ canonical_alias::RoomCanonicalAliasEventContent, create::RoomCreateEventContent, - encrypted::Relation, member::MembershipState, - power_levels::RoomPowerLevelsEventContent, redaction::RoomRedactionEventContent, + encrypted::Relation, member::MembershipState, redaction::RoomRedactionEventContent, }, GlobalAccountDataEventType, StateEventType, TimelineEventType, }, - push::{Action, Ruleset, Tweak}, + push::{Action, PushConditionPowerLevelsCtx, Ruleset, Tweak}, state_res::{self, Event}, uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId, ServerName, UserId, @@ -290,96 +289,99 @@ impl Service { drop(insert_lock); // See if the event matches any known pushers - let power_levels: RoomPowerLevelsEventContent = services() - .rooms - .state_accessor - .room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")? - .map(|ev| { - serde_json::from_str(ev.content.get()) - .map_err(|_| Error::bad_database("invalid m.room.power_levels event")) - }) - .transpose()? - .unwrap_or_default(); + // + // Will fail if this is the first event (the create event), which is fine, since it cannot + // notify anyone else anyways + if pdu.kind != TimelineEventType::RoomCreate + || pdu.state_key.as_deref().is_none_or(|key| !key.is_empty()) + { + if let Ok(power_levels) = services() + .rooms + .state_accessor + .power_levels(&pdu.room_id) + .map(PushConditionPowerLevelsCtx::from) + { + let sync_pdu = pdu.to_sync_room_event(); - let sync_pdu = pdu.to_sync_room_event(); + let mut notifies = Vec::new(); + let mut highlights = Vec::new(); - let mut notifies = Vec::new(); - let mut highlights = Vec::new(); + let mut push_target = services() + .rooms + .state_cache + .get_our_real_users(&pdu.room_id)?; - let mut push_target = services() - .rooms - .state_cache - .get_our_real_users(&pdu.room_id)?; + if pdu.kind == TimelineEventType::RoomMember { + if let Some(state_key) = &pdu.state_key { + let target_user_id = UserId::parse(state_key.clone()) + .expect("This state_key was previously validated"); - if pdu.kind == TimelineEventType::RoomMember { - if let Some(state_key) = &pdu.state_key { - let target_user_id = UserId::parse(state_key.clone()) - .expect("This state_key was previously validated"); - - if !push_target.contains(&target_user_id) { - let mut target = push_target.as_ref().clone(); - target.insert(target_user_id); - push_target = Arc::new(target); - } - } - } - - for user in push_target.iter() { - // Don't notify the user of their own events - if user == &pdu.sender { - continue; - } - - let rules_for_user = services() - .account_data - .get( - None, - user, - GlobalAccountDataEventType::PushRules.to_string().into(), - )? - .map(|event| { - serde_json::from_str::(event.get()) - .map_err(|_| Error::bad_database("Invalid push rules event in db.")) - }) - .transpose()? - .map(|ev: PushRulesEvent| ev.content.global) - .unwrap_or_else(|| Ruleset::server_default(user)); - - let mut highlight = false; - let mut notify = false; - - for action in services().pusher.get_actions( - user, - &rules_for_user, - &power_levels, - &sync_pdu, - &pdu.room_id, - )? { - match action { - Action::Notify => notify = true, - Action::SetTweak(Tweak::Highlight(true)) => { - highlight = true; + if !push_target.contains(&target_user_id) { + let mut target = push_target.as_ref().clone(); + target.insert(target_user_id); + push_target = Arc::new(target); + } } - _ => {} - }; - } + } - if notify { - notifies.push(user.clone()); - } + for user in push_target.iter() { + // Don't notify the user of their own events + if user == &pdu.sender { + continue; + } - if highlight { - highlights.push(user.clone()); - } + let rules_for_user = services() + .account_data + .get( + None, + user, + GlobalAccountDataEventType::PushRules.to_string().into(), + )? + .map(|event| { + serde_json::from_str::(event.get()) + .map_err(|_| Error::bad_database("Invalid push rules event in db.")) + }) + .transpose()? + .map(|ev: PushRulesEvent| ev.content.global) + .unwrap_or_else(|| Ruleset::server_default(user)); - for push_key in services().pusher.get_pushkeys(user) { - services().sending.send_push_pdu(&pdu_id, user, push_key?)?; + let mut highlight = false; + let mut notify = false; + + for action in services().pusher.get_actions( + user, + &rules_for_user, + power_levels.clone(), + &sync_pdu, + &pdu.room_id, + )? { + match action { + Action::Notify => notify = true, + Action::SetTweak(Tweak::Highlight(true)) => { + highlight = true; + } + _ => {} + }; + } + + if notify { + notifies.push(user.clone()); + } + + if highlight { + highlights.push(user.clone()); + } + + for push_key in services().pusher.get_pushkeys(user) { + services().sending.send_push_pdu(&pdu_id, user, push_key?)?; + } + } + + self.db + .increment_notification_counts(&pdu.room_id, notifies, highlights)?; } } - self.db - .increment_notification_counts(&pdu.room_id, notifies, highlights)?; - match pdu.kind { TimelineEventType::RoomRedaction => { let room_version_id = services().rooms.state.get_room_version(&pdu.room_id)?; @@ -1167,16 +1169,8 @@ impl Service { return Ok(()); } - let power_levels: RoomPowerLevelsEventContent = services() - .rooms - .state_accessor - .room_state_get(room_id, &StateEventType::RoomPowerLevels, "")? - .map(|ev| { - serde_json::from_str(ev.content.get()) - .map_err(|_| Error::bad_database("invalid m.room.power_levels event")) - }) - .transpose()? - .unwrap_or_default(); + let power_levels = services().rooms.state_accessor.power_levels(room_id)?; + let mut admin_servers = power_levels .users .iter()