atagen

macltok

public
Star0
Watch0

Commit e9af9e0e0f31

tarball

atagen <boss@atagen.co>2026-10-25 11:09 UTC

2 files changed+121-51
notifications: add only the highlight to the keyword's own rule

Reading NotificationSettings::ruleset to find the rule depended on the sdk's cache, and rewriting the actions added notify and a sound to a rule the user had made quiet. The sdk's add_keyword creates the content rule under the keyword as its id, so the rule is fetched by that id and gains a highlight tweak with its other actions kept.
Modified file: src/engine/commands.rs+42 additions−48 deletions
@@ -4,14 +4,15 @@ use futures_util::FutureExt;
44 use matrix_sdk::encryption::verification::VerificationRequest;
55 use matrix_sdk::{
66 Client,
7- notification_settings::{
8- IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode as SdkMode,
9- },
7+ notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode as SdkMode},
108 ruma::{
119 OwnedDeviceId, OwnedEventId, OwnedMxcUri, RoomId, TransactionId,
12- api::client::{push::set_pushrule_actions, receipt::create_receipt::v3::ReceiptType},
10+ api::client::{
11+ push::{get_pushrule, set_pushrule_actions},
12+ receipt::create_receipt::v3::ReceiptType,
13+ },
1314 events::receipt::ReceiptThread,
14- push::{Action, HighlightTweakValue, RuleKind, SoundTweakValue, Tweak},
15+ push::{Action, HighlightTweakValue, RuleKind, Tweak},
1516 },
1617 send_queue::{LocalEchoContent, SendHandle},
1718 };
@@ -140,7 +141,7 @@ async fn notification(client: &Client, command: NotificationCommand) -> Result<(
140141 }
141142 NotificationCommand::AddKeyword(keyword) => {
142143 match settings.add_keyword(keyword.clone()).await {
143- Ok(()) => return highlight_keyword(client, &settings, &keyword).await,
144+ Ok(()) => return highlight_keyword(client, &keyword).await,
144145 Err(error) => Err(error),
145146 }
146147 }
@@ -156,53 +157,46 @@ async fn notification(client: &Client, command: NotificationCommand) -> Result<(
156157 }
157158
158159 /// sdk keyword rules carry only notify and a sound, while Element's loud keywords also
159-/// highlight, which is what lets the inbox keep them as mentions
160-async fn highlight_keyword(
161- client: &Client,
162- settings: &NotificationSettings,
163- keyword: &str,
164-) -> Result<(), Error> {
165- let ruleset = settings.ruleset().await;
166- for rule in ruleset
167- .content
168- .iter()
169- .filter(|rule| !rule.default && rule.pattern == keyword)
170- .filter(|rule| !rule.actions.iter().any(Action::is_highlight))
160+/// highlight, which is what lets the inbox keep them as mentions. The sdk names a
161+/// keyword's content rule after the keyword itself, a rule under another id is left
162+/// alone.
163+async fn highlight_keyword(client: &Client, keyword: &str) -> Result<(), Error> {
164+ let failed = |source| Error::from_http("highlighting a notification keyword failed", source);
165+ let rule = match client
166+ .send(get_pushrule::v3::Request::new(
167+ RuleKind::Content,
168+ keyword.to_owned(),
169+ ))
170+ .await
171171 {
172- client
173- .send(set_pushrule_actions::v3::Request::new(
174- RuleKind::Content,
175- rule.rule_id.clone(),
176- keyword_actions(&rule.actions),
177- ))
178- .await
179- .map_err(|source| {
180- Error::from_sdk("highlighting a notification keyword failed", source.into())
181- })?;
172+ Ok(response) => response.rule,
173+ Err(error)
174+ if error
175+ .as_client_api_error()
176+ .is_some_and(|error| error.status_code.as_u16() == 404) =>
177+ {
178+ return Ok(());
179+ }
180+ Err(error) => return Err(failed(error)),
181+ };
182+ if rule.actions.iter().any(Action::is_highlight) {
183+ return Ok(());
182184 }
183- Ok(())
184-}
185-
186-fn keyword_actions(existing: &[Action]) -> Vec<Action> {
187- let mut actions: Vec<_> = existing
188- .iter()
185+ let mut actions: Vec<_> = rule
186+ .actions
187+ .into_iter()
189188 .filter(|action| !matches!(action, Action::SetTweak(Tweak::Highlight(_))))
190- .cloned()
191189 .collect();
192- if !actions
193- .iter()
194- .any(|action| matches!(action, Action::Notify))
195- {
196- actions.insert(0, Action::Notify);
197- }
198- if !actions
199- .iter()
200- .any(|action| matches!(action, Action::SetTweak(Tweak::Sound(_))))
201- {
202- actions.push(Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)));
203- }
204190 actions.push(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)));
205- actions
191+ client
192+ .send(set_pushrule_actions::v3::Request::new(
193+ RuleKind::Content,
194+ keyword.to_owned(),
195+ actions,
196+ ))
197+ .await
198+ .map_err(failed)?;
199+ Ok(())
206200 }
207201
208202 const fn sdk_mode(mode: RoomNotificationMode) -> SdkMode {
Modified file: tests/mentions_inbox.rs+79 additions−3 deletions
@@ -28,7 +28,7 @@ use support::receive_until;
2828 use support::shut_down;
2929 use wiremock::{
3030 Mock, Request, Respond, ResponseTemplate,
31- matchers::{body_string_contains, method, path, path_regex, query_param},
31+ matchers::{method, path, path_regex, query_param},
3232 };
3333
3434 #[cfg(feature = "serde")]
@@ -864,6 +864,26 @@ async fn highlighting_keywords_are_mentions_and_quiet_ones_are_not() {
864864 shut_down(&handle, &mut events, running).await;
865865 }
866866
867+fn actions_are(expected: Value) -> impl Fn(&Request) -> bool + Send + Sync + 'static {
868+ move |request: &Request| {
869+ serde_json::from_slice::<Value>(&request.body).is_ok_and(|body| body["actions"] == expected)
870+ }
871+}
872+
873+async fn mount_keyword_rule(server: &MatrixMockServer, actions: Value) {
874+ Mock::given(method("GET"))
875+ .and(path("/_matrix/client/v3/pushrules/global/content/deploy"))
876+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
877+ "rule_id": "deploy",
878+ "pattern": "deploy",
879+ "default": false,
880+ "enabled": true,
881+ "actions": actions,
882+ })))
883+ .mount(server.server())
884+ .await;
885+}
886+
867887 #[tokio::test]
868888 async fn adding_a_keyword_gives_its_rule_a_highlight() {
869889 let server = MatrixMockServer::new().await;
@@ -875,12 +895,68 @@ async fn adding_a_keyword_gives_its_rule_a_highlight() {
875895 .expect(1)
876896 .mount(server.server())
877897 .await;
898+ mount_keyword_rule(
899+ &server,
900+ json!(["notify", {"set_tweak": "sound", "value": "default"}]),
901+ )
902+ .await;
878903 Mock::given(method("PUT"))
879904 .and(path(
880905 "/_matrix/client/v3/pushrules/global/content/deploy/actions",
881906 ))
882- .and(body_string_contains("highlight"))
883- .and(body_string_contains("notify"))
907+ .and(actions_are(json!([
908+ "notify",
909+ {"set_tweak": "sound", "value": "default"},
910+ {"set_tweak": "highlight"},
911+ ])))
912+ .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
913+ .expect(1)
914+ .mount(server.server())
915+ .await;
916+ SlidingFeed::new(room_id.to_owned(), Vec::new())
917+ .mount(&server)
918+ .await;
919+
920+ let (engine, handle, mut events) = Engine::from_client(client, EngineConfig::default())
921+ .await
922+ .unwrap();
923+ let running = tokio::spawn(engine.run());
924+ handle.add_notification_keyword("deploy").await.unwrap();
925+ shut_down(&handle, &mut events, running).await;
926+ server.server().verify().await;
927+}
928+
929+#[tokio::test]
930+async fn a_quiet_keyword_rule_only_gains_the_highlight() {
931+ let server = MatrixMockServer::new().await;
932+ let client = server.client_builder().build().await;
933+ let own_user = client.user_id().unwrap().to_owned();
934+ let room_id = room_id!("!quiet-keyword-rule:localhost");
935+ let mut rules = Ruleset::server_default(&own_user);
936+ rules.content.insert(
937+ NewPatternedPushRule::new("deploy".to_owned(), "deploy".to_owned(), Vec::new()).into(),
938+ );
939+ server
940+ .mock_sync()
941+ .ok_and_run(&client, {
942+ let own_user = own_user.clone();
943+ move |builder| {
944+ let factory = EventFactory::new().room(room_id);
945+ builder.add_global_account_data(factory.push_rules(rules));
946+ builder.add_joined_room(
947+ JoinedRoomBuilder::new(room_id).add_state_event(factory.member(&own_user)),
948+ );
949+ }
950+ })
951+ .await;
952+ mount_keyword_rule(&server, json!([])).await;
953+ Mock::given(method("PUT"))
954+ .and(path(
955+ "/_matrix/client/v3/pushrules/global/content/deploy/actions",
956+ ))
957+ .and(actions_are(
958+ json!([{"set_tweak": "highlight"}]),
959+ ))
884960 .respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
885961 .expect(1)
886962 .mount(server.server())