From 02fe030b2aec3e264b47fd1e7b7db847fa5f94f4 Mon Sep 17 00:00:00 2001 From: timokoesters Date: Fri, 12 Jun 2020 13:18:25 +0200 Subject: [PATCH] improvement: better default push rules --- src/client_server.rs | 120 ++++++++++------------ src/database/rooms.rs | 79 +++++++++++--- src/main.rs | 2 + src/pdu.rs | 6 ++ src/push_rules.rs | 232 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 359 insertions(+), 80 deletions(-) create mode 100644 src/push_rules.rs diff --git a/src/client_server.rs b/src/client_server.rs index e49bfb4..0676abc 100644 --- a/src/client_server.rs +++ b/src/client_server.rs @@ -204,33 +204,7 @@ pub fn register_route( &EventType::PushRules, serde_json::to_value(ruma::events::push_rules::PushRulesEvent { content: ruma::events::push_rules::PushRulesEventContent { - global: ruma::events::push_rules::Ruleset { - content: vec![], - override_: vec![ruma::events::push_rules::ConditionalPushRule { - actions: vec![ruma::events::push_rules::Action::DontNotify], - default: true, - enabled: false, - rule_id: ".m.rule.master".to_owned(), - conditions: vec![], - }], - room: vec![], - sender: vec![], - underride: vec![ruma::events::push_rules::ConditionalPushRule { - actions: vec![ - ruma::events::push_rules::Action::Notify, - ruma::events::push_rules::Action::SetTweak(ruma::push::Tweak::Sound( - "default".to_owned(), - )), - ], - default: true, - enabled: true, - rule_id: ".m.rule.message".to_owned(), - conditions: vec![ruma::events::push_rules::PushCondition::EventMatch { - key: "type".to_owned(), - pattern: "m.room.message".to_owned(), - }], - }], - }, + global: crate::push_rules::default_pushrules(&user_id), }, }) .expect("data is valid, we just created it") @@ -502,8 +476,7 @@ pub fn set_displayname_route( displayname: body.displayname.clone(), ..serde_json::from_value::>( db.rooms - .room_state(&room_id)? - .get(&(EventType::RoomMember, user_id.to_string())) + .room_state_get(&room_id, &EventType::RoomMember, &user_id.to_string())? .ok_or_else(|| { Error::bad_database( "Tried to send displayname update for user not in the room.", @@ -593,8 +566,7 @@ pub fn set_avatar_url_route( avatar_url: body.avatar_url.clone(), ..serde_json::from_value::>( db.rooms - .room_state(&room_id)? - .get(&(EventType::RoomMember, user_id.to_string())) + .room_state_get(&room_id, &EventType::RoomMember, &user_id.to_string())? .ok_or_else(|| { Error::bad_database( "Tried to send avatar url update for user not in the room.", @@ -1267,8 +1239,7 @@ pub fn join_room_by_id_route( let event = db .rooms - .room_state(&body.room_id)? - .get(&(EventType::RoomMember, user_id.to_string())) + .room_state_get(&body.room_id, &EventType::RoomMember, &user_id.to_string())? .map_or_else( || { // There was no existing membership event @@ -1348,11 +1319,10 @@ pub fn leave_room_route( _room_id: String, ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - let state = db.rooms.room_state(&body.room_id)?; let mut event = serde_json::from_value::>( - state - .get(&(EventType::RoomMember, user_id.to_string())) + db.rooms + .room_state_get(&body.room_id, &EventType::RoomMember, &user_id.to_string())? .ok_or(Error::BadRequest( ErrorKind::BadState, "Cannot leave a room you are not a member of.", @@ -1387,12 +1357,11 @@ pub fn kick_user_route( _room_id: String, ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - let state = db.rooms.room_state(&body.room_id)?; let mut event = serde_json::from_value::>( - state - .get(&(EventType::RoomMember, user_id.to_string())) + db.rooms + .room_state_get(&body.room_id, &EventType::RoomMember, &user_id.to_string())? .ok_or(Error::BadRequest( ErrorKind::BadState, "Cannot kick member that's not in the room.", @@ -1428,12 +1397,12 @@ pub fn ban_user_route( _room_id: String, ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - let state = db.rooms.room_state(&body.room_id)?; // TODO: reason - let event = state - .get(&(EventType::RoomMember, user_id.to_string())) + let event = db + .rooms + .room_state_get(&body.room_id, &EventType::RoomMember, &user_id.to_string())? .map_or( Ok::<_, Error>(member::MemberEventContent { membership: member::MembershipState::Ban, @@ -1475,12 +1444,11 @@ pub fn unban_user_route( _room_id: String, ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - let state = db.rooms.room_state(&body.room_id)?; let mut event = serde_json::from_value::>( - state - .get(&(EventType::RoomMember, user_id.to_string())) + db.rooms + .room_state_get(&body.room_id, &EventType::RoomMember, &user_id.to_string())? .ok_or(Error::BadRequest( ErrorKind::BadState, "Cannot unban a user who is not banned.", @@ -1642,7 +1610,8 @@ pub async fn get_public_rooms_filtered_route( .map(|room_id| { let room_id = room_id?; - let state = db.rooms.room_state(&room_id)?; + // TODO: Do not load full state? + let state = db.rooms.room_state_full(&room_id)?; let chunk = directory::PublicRoomsChunk { aliases: Vec::new(), @@ -1775,9 +1744,29 @@ pub fn search_users_route( } #[get("/_matrix/client/r0/rooms/<_room_id>/members")] -pub fn get_member_events_route(_room_id: String) -> ConduitResult { - warn!("TODO: get_member_events_route"); - Ok(get_member_events::Response { chunk: Vec::new() }.into()) +pub fn get_member_events_route( + db: State<'_, Database>, + //body: Ruma, + _room_id: String, +) -> ConduitResult { + //let user_id = body.user_id.as_ref().expect("user is authenticated"); + + //if !db.rooms.is_joined(user_id, &body.room_id)? { + // return Err(Error::BadRequest( + // ErrorKind::Forbidden, + // "You don't have permission to view this room.", + // )); + //} + + Ok(get_member_events::Response { + chunk: Vec::new(),/*db + .rooms + .room_state_type(&body.room_id, &EventType::RoomMember)? + .values() + .map(|pdu| pdu.to_member_event()) + .collect(),*/ + } + .into()) } #[get("/_matrix/client/r0/thirdparty/protocols")] @@ -1951,7 +1940,7 @@ pub fn get_state_events_route( Ok(get_state_events::Response { room_state: db .rooms - .room_state(&body.room_id)? + .room_state_full(&body.room_id)? .values() .map(|pdu| pdu.to_state_event()) .collect(), @@ -1979,10 +1968,9 @@ pub fn get_state_events_for_key_route( )); } - let state = db.rooms.room_state(&body.room_id)?; - - let event = state - .get(&(body.event_type.clone(), body.state_key.clone())) + let event = db + .rooms + .room_state_get(&body.room_id, &body.event_type, &body.state_key)? .ok_or(Error::BadRequest( ErrorKind::NotFound, "State event not found.", @@ -2014,17 +2002,16 @@ pub fn get_state_events_for_empty_key_route( )); } - let state = db.rooms.room_state(&body.room_id)?; - - let event = state - .get(&(body.event_type.clone(), "".to_owned())) + let event = db + .rooms + .room_state_get(&body.room_id, &body.event_type, "")? .ok_or(Error::BadRequest( ErrorKind::NotFound, "State event not found.", ))?; Ok(get_state_events_for_empty_key::Response { - content: serde_json::value::to_raw_value(event) + content: serde_json::value::to_raw_value(&event) .map_err(|_| Error::bad_database("Invalid event content in database"))?, } .into()) @@ -2068,7 +2055,7 @@ pub fn sync_route( let content = serde_json::from_value::< EventJson, >(pdu.content.clone()) - .map_err(|_| Error::bad_database("Invalid PDU in database."))? + .expect("EventJson::from_value always works") .deserialize() .map_err(|_| Error::bad_database("Invalid PDU in database."))?; if content.membership == ruma::events::room::member::MembershipState::Join { @@ -2081,7 +2068,7 @@ pub fn sync_route( } } - let state = db.rooms.room_state(&room_id)?; + let members = db.rooms.room_state_type(&room_id, &EventType::RoomMember)?; let (joined_member_count, invited_member_count, heroes) = if send_member_count { let joined_member_count = db.rooms.room_members(&room_id).count(); @@ -2111,8 +2098,8 @@ pub fn sync_route( let current_content = serde_json::from_value::< EventJson, >( - state - .get(&(EventType::RoomMember, state_key.clone())) + members + .get(state_key) .ok_or_else(|| { Error::bad_database( "A user that joined once has no member event anymore.", @@ -2264,7 +2251,8 @@ pub fn sync_route( // TODO: state before timeline state: sync_events::State { events: if joined_since_last_sync { - state + db.rooms + .room_state_full(&room_id)? .into_iter() .map(|(_, pdu)| pdu.to_state_event()) .collect() @@ -2337,7 +2325,7 @@ pub fn sync_route( invite_state: sync_events::InviteState { events: db .rooms - .room_state(&room_id)? + .room_state_full(&room_id)? .into_iter() .map(|(_, pdu)| pdu.to_stripped_state_event()) .collect(), @@ -2496,7 +2484,7 @@ pub fn get_context_route( events_after, state: db // TODO: State at event .rooms - .room_state(&body.room_id)? + .room_state_full(&body.room_id)? .values() .map(|pdu| pdu.to_state_event()) .collect(), diff --git a/src/database/rooms.rs b/src/database/rooms.rs index ee14fc8..604b5aa 100644 --- a/src/database/rooms.rs +++ b/src/database/rooms.rs @@ -56,7 +56,10 @@ impl Rooms { } /// Returns the full room state. - pub fn room_state(&self, room_id: &RoomId) -> Result> { + pub fn room_state_full( + &self, + room_id: &RoomId, + ) -> Result> { let mut hashmap = HashMap::new(); for pdu in self .roomstateid_pdu @@ -78,6 +81,58 @@ impl Rooms { Ok(hashmap) } + /// Returns the full room state. + pub fn room_state_type( + &self, + room_id: &RoomId, + event_type: &EventType, + ) -> Result> { + let mut prefix = room_id.to_string().as_bytes().to_vec(); + prefix.push(0xff); + prefix.extend_from_slice(&event_type.to_string().as_bytes()); + + let mut hashmap = HashMap::new(); + for pdu in self + .roomstateid_pdu + .scan_prefix(&prefix) + .values() + .map(|value| { + Ok::<_, Error>( + serde_json::from_slice::(&value?) + .map_err(|_| Error::bad_database("Invalid PDU in db."))?, + ) + }) + { + let pdu = pdu?; + let state_key = pdu.state_key.clone().ok_or_else(|| { + Error::bad_database("Room state contains event without state_key.") + })?; + hashmap.insert(state_key, pdu); + } + Ok(hashmap) + } + + /// Returns the full room state. + pub fn room_state_get( + &self, + room_id: &RoomId, + event_type: &EventType, + state_key: &str, + ) -> Result> { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&event_type.to_string().as_bytes()); + key.push(0xff); + key.extend_from_slice(&state_key.as_bytes()); + + self.roomstateid_pdu.get(&key)?.map_or(Ok(None), |value| { + Ok::<_, Error>(Some( + serde_json::from_slice::(&value) + .map_err(|_| Error::bad_database("Invalid PDU in db."))?, + )) + }) + } + /// Returns the `count` of this pdu's id. pub fn get_pdu_count(&self, event_id: &EventId) -> Result> { self.eventid_pduid @@ -212,8 +267,7 @@ impl Rooms { // Is the event authorized? if let Some(state_key) = &state_key { let power_levels = self - .room_state(&room_id)? - .get(&(EventType::RoomPowerLevels, "".to_owned())) + .room_state_get(&room_id, &EventType::RoomPowerLevels, "")? .map_or_else( || { Ok::<_, Error>(power_levels::PowerLevelsEventContent { @@ -244,8 +298,7 @@ impl Rooms { }, )?; let sender_membership = self - .room_state(&room_id)? - .get(&(EventType::RoomMember, sender.to_string())) + .room_state_get(&room_id, &EventType::RoomMember, &sender.to_string())? .map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| { Ok( serde_json::from_value::>( @@ -280,8 +333,11 @@ impl Rooms { })?; let current_membership = self - .room_state(&room_id)? - .get(&(EventType::RoomMember, target_user_id.to_string())) + .room_state_get( + &room_id, + &EventType::RoomMember, + &target_user_id.to_string(), + )? .map_or(Ok::<_, Error>(member::MembershipState::Leave), |pdu| { Ok( serde_json::from_value::>( @@ -315,8 +371,7 @@ impl Rooms { ); let join_rules = - self.room_state(&room_id)? - .get(&(EventType::RoomJoinRules, "".to_owned())) + self.room_state_get(&room_id, &EventType::RoomJoinRules, "")? .map_or(Ok::<_, Error>(join_rules::JoinRule::Public), |pdu| { Ok(serde_json::from_value::< EventJson, @@ -446,12 +501,8 @@ impl Rooms { + 1; let mut unsigned = unsigned.unwrap_or_default(); - // TODO: Optimize this to not load the whole room state? if let Some(state_key) = &state_key { - if let Some(prev_pdu) = self - .room_state(&room_id)? - .get(&(event_type.clone(), state_key.to_owned())) - { + if let Some(prev_pdu) = self.room_state_get(&room_id, &event_type, &state_key)? { unsigned.insert("prev_content".to_owned(), prev_pdu.content.clone()); } } diff --git a/src/main.rs b/src/main.rs index 8b68e36..5c51ae0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![feature(proc_macro_hygiene, decl_macro)] #![warn(rust_2018_idioms)] +pub mod push_rules; + mod client_server; mod database; mod error; diff --git a/src/pdu.rs b/src/pdu.rs index f496933..8a5858e 100644 --- a/src/pdu.rs +++ b/src/pdu.rs @@ -4,6 +4,7 @@ use ruma::{ api::federation::EventHash, events::{ collections::all::{RoomEvent, StateEvent}, + room::member::MemberEvent, stripped::AnyStrippedStateEvent, EventJson, EventType, }, @@ -95,4 +96,9 @@ impl PduEvent { serde_json::from_str::>(&json) .expect("EventJson::from_str always works") } + pub fn to_member_event(&self) -> EventJson { + let json = serde_json::to_string(&self).expect("PDUs are always valid"); + serde_json::from_str::>(&json) + .expect("EventJson::from_str always works") + } } diff --git a/src/push_rules.rs b/src/push_rules.rs new file mode 100644 index 0000000..44c9363 --- /dev/null +++ b/src/push_rules.rs @@ -0,0 +1,232 @@ +use ruma::{ + events::push_rules::{ + ConditionalPushRule, PatternedPushRule, PushCondition, PushRule, Ruleset, + }, + identifiers::UserId, + push::{Action, Tweak}, +}; + +pub fn default_pushrules(user_id: &UserId) -> Ruleset { + Ruleset { + content: vec![contains_user_name_rule(&user_id)], + override_: vec![ + master_rule(), + suppress_notices_rule(), + invite_for_me_rule(), + member_event_rule(), + contains_display_name_rule(), + tombstone_rule(), + roomnotif_rule(), + ], + room: vec![], + sender: vec![], + underride: vec![ + call_rule(), + encrypted_room_one_to_one_rule(), + room_one_to_one_rule(), + message_rule(), + encrypted_rule(), + ], + } +} + +pub fn master_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::DontNotify], + default: true, + enabled: false, + rule_id: ".m.rule.master".to_owned(), + conditions: vec![], + } +} + +pub fn suppress_notices_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::DontNotify], + default: true, + enabled: true, + rule_id: ".m.rule.suppress_notices".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "content.msgtype".to_owned(), + pattern: "m.notice".to_owned(), + }], + } +} + +pub fn invite_for_me_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("default".to_owned())), + Action::SetTweak(Tweak::Highlight(false)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.invite_for_me".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "content.membership".to_owned(), + pattern: "m.invite".to_owned(), + }], + } +} + +pub fn member_event_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::DontNotify], + default: true, + enabled: true, + rule_id: ".m.rule.member_event".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "content.membership".to_owned(), + pattern: "type".to_owned(), + }], + } +} + +pub fn contains_display_name_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("default".to_owned())), + Action::SetTweak(Tweak::Highlight(true)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.contains_display_name".to_owned(), + conditions: vec![PushCondition::ContainsDisplayName], + } +} + +pub fn tombstone_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], + default: true, + enabled: true, + rule_id: ".m.rule.tombstone".to_owned(), + conditions: vec![ + PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.room.tombstone".to_owned(), + }, + PushCondition::EventMatch { + key: "state_key".to_owned(), + pattern: "".to_owned(), + }, + ], + } +} + +pub fn roomnotif_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(true))], + default: true, + enabled: true, + rule_id: ".m.rule.roomnotif".to_owned(), + conditions: vec![ + PushCondition::EventMatch { + key: "content.body".to_owned(), + pattern: "@room".to_owned(), + }, + PushCondition::SenderNotificationPermission { + key: "room".to_owned(), + }, + ], + } +} + +pub fn contains_user_name_rule(user_id: &UserId) -> PatternedPushRule { + PatternedPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("default".to_owned())), + Action::SetTweak(Tweak::Highlight(true)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.contains_user_name".to_owned(), + pattern: user_id.localpart().to_owned(), + } +} + +pub fn call_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("ring".to_owned())), + Action::SetTweak(Tweak::Highlight(false)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.call".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.call.invite".to_owned(), + }], + } +} + +pub fn encrypted_room_one_to_one_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("default".to_owned())), + Action::SetTweak(Tweak::Highlight(false)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.encrypted_room_one_to_one".to_owned(), + conditions: vec![ + PushCondition::RoomMemberCount { is: "2".to_owned() }, + PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.room.encrypted".to_owned(), + }, + ], + } +} + +pub fn room_one_to_one_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![ + Action::Notify, + Action::SetTweak(Tweak::Sound("default".to_owned())), + Action::SetTweak(Tweak::Highlight(false)), + ], + default: true, + enabled: true, + rule_id: ".m.rule.room_one_to_one".to_owned(), + conditions: vec![ + PushCondition::RoomMemberCount { is: "2".to_owned() }, + PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.room.message".to_owned(), + }, + ], + } +} + +pub fn message_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(false))], + default: true, + enabled: true, + rule_id: ".m.rule.message".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.room.message".to_owned(), + }], + } +} + +pub fn encrypted_rule() -> ConditionalPushRule { + ConditionalPushRule { + actions: vec![Action::Notify, Action::SetTweak(Tweak::Highlight(false))], + default: true, + enabled: true, + rule_id: ".m.rule.encrypted".to_owned(), + conditions: vec![PushCondition::EventMatch { + key: "type".to_owned(), + pattern: "m.room.encrypted".to_owned(), + }], + } +}