Skip to content

Commit 3ad582c

Browse files
author
Evan
committed
normalize predefined push rule actions to match spec defaults
1 parent abc6b4a commit 3ad582c

File tree

3 files changed

+146
-9
lines changed

3 files changed

+146
-9
lines changed

crates/matrix-sdk-base/src/client.rs

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,42 @@ use crate::{
7272
sync::{RoomUpdates, SyncResponse},
7373
};
7474

75+
/// Normalizes the actions of predefined push rules to match the spec-defined
76+
/// defaults from [`Ruleset::server_default`].
77+
///
78+
/// Some homeservers provide predefined push rules with actions that differ from
79+
/// the Matrix spec (e.g., missing sound tweaks on `.m.rule.room_one_to_one`).
80+
/// This ensures clients produce correct notification behavior (such as playing
81+
/// sounds for 1:1 messages) regardless of the homeserver implementation.
82+
///
83+
/// Only rules marked as `default: true` are affected. The `enabled` state from
84+
/// the server is always preserved.
85+
pub fn normalize_predefined_push_rule_actions(rules: &mut Ruleset, user_id: &UserId) {
86+
let defaults = Ruleset::server_default(user_id);
87+
88+
// Replace actions of server-default rules with spec-defined actions.
89+
// We iterate the spec defaults (small, fixed-size) and look up each in the
90+
// server rules by rule_id, avoiding any allocation.
91+
macro_rules! normalize_rules {
92+
($rules:expr, $defaults:expr, $field:ident) => {
93+
for default_rule in &$defaults.$field {
94+
if let Some(server_rule) = $rules.$field.get(default_rule.rule_id.as_str()) {
95+
if server_rule.default {
96+
let enabled = server_rule.enabled;
97+
let mut replacement = default_rule.clone();
98+
replacement.enabled = enabled;
99+
$rules.$field.replace(replacement);
100+
}
101+
}
102+
}
103+
};
104+
}
105+
106+
normalize_rules!(rules, defaults, override_);
107+
normalize_rules!(rules, defaults, underride);
108+
normalize_rules!(rules, defaults, content);
109+
}
110+
75111
/// A no (network) IO client implementation.
76112
///
77113
/// This client is a state machine that receives responses and events and
@@ -1065,14 +1101,22 @@ impl BaseClient {
10651101
.push_rules()
10661102
.and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
10671103
{
1068-
Ok(event.content.global)
1104+
let mut rules = event.content.global;
1105+
if let Some(session_meta) = self.state_store.session_meta() {
1106+
normalize_predefined_push_rule_actions(&mut rules, &session_meta.user_id);
1107+
}
1108+
Ok(rules)
10691109
} else if let Some(event) = self
10701110
.state_store
10711111
.get_account_data_event_static::<PushRulesEventContent>()
10721112
.await?
10731113
.and_then(|ev| ev.deserialize().ok())
10741114
{
1075-
Ok(event.content.global)
1115+
let mut rules = event.content.global;
1116+
if let Some(session_meta) = self.state_store.session_meta() {
1117+
normalize_predefined_push_rule_actions(&mut rules, &session_meta.user_id);
1118+
}
1119+
Ok(rules)
10761120
} else if let Some(session_meta) = self.state_store.session_meta() {
10771121
Ok(Ruleset::server_default(&session_meta.user_id))
10781122
} else {
@@ -1828,4 +1872,94 @@ mod tests {
18281872
client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
18291873
);
18301874
}
1875+
1876+
#[test]
1877+
fn test_normalize_predefined_push_rule_actions_restores_sound_tweaks() {
1878+
use ruma::push::{Action, Ruleset, Tweak};
1879+
1880+
let user_id = user_id!("@user:example.com");
1881+
1882+
// Simulate a server that provides rules without sound tweaks (like
1883+
// Continuwuity).
1884+
let mut server_rules = Ruleset::server_default(user_id);
1885+
server_rules.underride = server_rules
1886+
.underride
1887+
.into_iter()
1888+
.map(|mut rule| {
1889+
if rule.rule_id == ".m.rule.room_one_to_one"
1890+
|| rule.rule_id == ".m.rule.encrypted_room_one_to_one"
1891+
{
1892+
rule.actions.retain(|a| !matches!(a, Action::SetTweak(Tweak::Sound(_))));
1893+
}
1894+
rule
1895+
})
1896+
.collect();
1897+
1898+
// Verify sound tweak was removed.
1899+
let rule = server_rules.underride.get(".m.rule.room_one_to_one").unwrap();
1900+
assert!(!rule.actions.iter().any(|a| a.sound().is_some()));
1901+
1902+
// Normalize should restore the spec-defined actions.
1903+
super::normalize_predefined_push_rule_actions(&mut server_rules, user_id);
1904+
1905+
let rule = server_rules.underride.get(".m.rule.room_one_to_one").unwrap();
1906+
assert!(rule.actions.iter().any(|a| a.sound().is_some()), "Sound tweak should be restored");
1907+
1908+
let rule = server_rules.underride.get(".m.rule.encrypted_room_one_to_one").unwrap();
1909+
assert!(rule.actions.iter().any(|a| a.sound().is_some()), "Sound tweak should be restored");
1910+
}
1911+
1912+
#[test]
1913+
fn test_normalize_predefined_push_rule_actions_preserves_enabled_state() {
1914+
use ruma::push::Ruleset;
1915+
1916+
let user_id = user_id!("@user:example.com");
1917+
1918+
let mut server_rules = Ruleset::server_default(user_id);
1919+
1920+
// Simulate user disabling a rule.
1921+
server_rules.underride = server_rules
1922+
.underride
1923+
.into_iter()
1924+
.map(|mut rule| {
1925+
if rule.rule_id == ".m.rule.room_one_to_one" {
1926+
rule.enabled = false;
1927+
}
1928+
rule
1929+
})
1930+
.collect();
1931+
1932+
super::normalize_predefined_push_rule_actions(&mut server_rules, user_id);
1933+
1934+
let rule = server_rules.underride.get(".m.rule.room_one_to_one").unwrap();
1935+
assert!(!rule.enabled, "Enabled state should be preserved from server");
1936+
}
1937+
1938+
#[test]
1939+
fn test_normalize_predefined_push_rule_actions_skips_non_default_rules() {
1940+
use ruma::push::{Action, ConditionalPushRuleInit, PushCondition, Ruleset};
1941+
1942+
let user_id = user_id!("@user:example.com");
1943+
let mut server_rules = Ruleset::server_default(user_id);
1944+
1945+
// Add a user-defined rule (default = false).
1946+
let user_rule = ConditionalPushRuleInit {
1947+
actions: vec![Action::Notify],
1948+
default: false,
1949+
enabled: true,
1950+
rule_id: "custom.user.rule".to_owned(),
1951+
conditions: vec![PushCondition::EventMatch {
1952+
key: "type".to_owned(),
1953+
pattern: "m.custom".to_owned(),
1954+
}],
1955+
};
1956+
server_rules.underride.insert(user_rule.into());
1957+
1958+
super::normalize_predefined_push_rule_actions(&mut server_rules, user_id);
1959+
1960+
// User rule should still exist with its original actions.
1961+
let rule = server_rules.underride.get("custom.user.rule").unwrap();
1962+
assert_eq!(rule.actions.len(), 1);
1963+
assert!(rule.actions.iter().any(|a| a.should_notify()));
1964+
}
18311965
}

crates/matrix-sdk-base/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub mod recent_emojis;
5151
#[cfg(feature = "uniffi")]
5252
uniffi::setup_scaffolding!();
5353

54-
pub use client::{BaseClient, ThreadingSupport};
54+
pub use client::{BaseClient, ThreadingSupport, normalize_predefined_push_rule_actions};
5555
#[cfg(any(test, feature = "testing"))]
5656
pub use http;
5757
#[cfg(feature = "e2e-encryption")]

crates/matrix-sdk/src/account.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,9 +1107,14 @@ impl Account {
11071107
/// If no push rules event was found, or it fails to deserialize, a ruleset
11081108
/// with the server-default push rules is returned.
11091109
///
1110+
/// Predefined push rule actions are normalized to match the spec-defined
1111+
/// defaults, ensuring correct notification behavior regardless of the
1112+
/// homeserver implementation.
1113+
///
11101114
/// Panics if called when the client is not logged in.
11111115
pub async fn push_rules(&self) -> Result<Ruleset> {
1112-
Ok(self
1116+
let user_id = self.client.user_id().expect("The client should be logged in");
1117+
let mut rules = self
11131118
.account_data::<PushRulesEventContent>()
11141119
.await?
11151120
.and_then(|r| match r.deserialize() {
@@ -1119,11 +1124,9 @@ impl Account {
11191124
None
11201125
}
11211126
})
1122-
.unwrap_or_else(|| {
1123-
Ruleset::server_default(
1124-
self.client.user_id().expect("The client should be logged in"),
1125-
)
1126-
}))
1127+
.unwrap_or_else(|| Ruleset::server_default(user_id));
1128+
matrix_sdk_base::normalize_predefined_push_rule_actions(&mut rules, user_id);
1129+
Ok(rules)
11271130
}
11281131

11291132
/// Retrieves the user's recently visited room list

0 commit comments

Comments
 (0)