@@ -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}
0 commit comments