From b8a61cfc17bba3d327913cb225ed38e263b2b13f Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:21:35 +0100 Subject: [PATCH] feat(WidgetDriver): Support widget redacts (#3987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changelog: Implement proper redact handling in the widget driver. This allows the Rust SDK widget driver to support widgets that rely on redacting. Co-authored-by: Damir Jelić --- crates/matrix-sdk/src/widget/machine/mod.rs | 14 ++- .../src/widget/machine/tests/error.rs | 19 +++- crates/matrix-sdk/src/widget/matrix.rs | 21 +++- crates/matrix-sdk/tests/integration/widget.rs | 96 ++++++++++++------- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/crates/matrix-sdk/src/widget/machine/mod.rs b/crates/matrix-sdk/src/widget/machine/mod.rs index 31e96fc7d..eb0c52187 100644 --- a/crates/matrix-sdk/src/widget/machine/mod.rs +++ b/crates/matrix-sdk/src/widget/machine/mod.rs @@ -295,7 +295,10 @@ impl WidgetMachine { ReadEventRequest::ReadMessageLikeEvent { event_type, limit } => { let filter_fn = |f: &EventFilter| f.matches_message_like_event_type(&event_type); if !capabilities.read.iter().any(filter_fn) { - return Some(self.send_from_widget_error_response(raw_request, "Not allowed")); + return Some(self.send_from_widget_error_response( + raw_request, + "Not allowed to read message like event", + )); } const DEFAULT_EVENT_LIMIT: u32 = 50; @@ -345,7 +348,10 @@ impl WidgetMachine { }); action } else { - Some(self.send_from_widget_error_response(raw_request, "Not allowed")) + Some(self.send_from_widget_error_response( + raw_request, + "Not allowed to read state event", + )) } } } @@ -381,7 +387,9 @@ impl WidgetMachine { )); } if !capabilities.send.iter().any(|filter| filter.matches(&filter_in)) { - return Some(self.send_from_widget_error_response(raw_request, "Not allowed")); + return Some( + self.send_from_widget_error_response(raw_request, "Not allowed to send event"), + ); } let (request, action) = self.send_matrix_driver_request(request); diff --git a/crates/matrix-sdk/src/widget/machine/tests/error.rs b/crates/matrix-sdk/src/widget/machine/tests/error.rs index 5db05727b..ccce797d4 100644 --- a/crates/matrix-sdk/src/widget/machine/tests/error.rs +++ b/crates/matrix-sdk/src/widget/machine/tests/error.rs @@ -98,7 +98,10 @@ fn read_request_for_non_allowed_message_like_events() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read message like event" + ); } #[test] @@ -124,7 +127,10 @@ fn read_request_for_non_allowed_state_events() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read state event" + ); } #[test] @@ -156,7 +162,7 @@ fn send_request_for_non_allowed_state_events() { assert_eq!(request_id, "send-me-a-message"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "send_event"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed to send event"); } #[test] @@ -188,7 +194,7 @@ fn send_request_for_non_allowed_message_like_events() { assert_eq!(request_id, "send-me-a-message"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "send_event"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed to send event"); } #[test] @@ -218,5 +224,8 @@ fn read_request_for_message_like_with_disallowed_msg_type_fails() { assert_eq!(request_id, "get-me-some-messages"); assert_eq!(msg["api"], "fromWidget"); assert_eq!(msg["action"], "org.matrix.msc2876.read_events"); - assert_eq!(msg["response"]["error"]["message"].as_str().unwrap(), "Not allowed"); + assert_eq!( + msg["response"]["error"]["message"].as_str().unwrap(), + "Not allowed to read message like event" + ); } diff --git a/crates/matrix-sdk/src/widget/matrix.rs b/crates/matrix-sdk/src/widget/matrix.rs index 96a40d423..e30e342d6 100644 --- a/crates/matrix-sdk/src/widget/matrix.rs +++ b/crates/matrix-sdk/src/widget/matrix.rs @@ -29,10 +29,10 @@ use ruma::{ AnyMessageLikeEventContent, AnyStateEventContent, AnySyncTimelineEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType, TimelineEventType, }, - serde::Raw, - RoomId, TransactionId, + serde::{from_raw_json_value, Raw}, + EventId, RoomId, TransactionId, }; -use serde_json::value::RawValue as RawJsonValue; +use serde_json::{value::RawValue as RawJsonValue, Value}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; use tracing::error; @@ -107,7 +107,11 @@ impl MatrixDriver { Ok(events) } - /// Sends a given `event` to the room. + /// Sends the given `event` to the room. + /// + /// This method allows the widget machine to handle widget requests by + /// providing a unified, high-level widget-specific API for sending events + /// to the room. pub(crate) async fn send( &self, event_type: TimelineEventType, @@ -116,6 +120,15 @@ impl MatrixDriver { delayed_event_parameters: Option, ) -> Result { let type_str = event_type.to_string(); + + if let Some(redacts) = from_raw_json_value::(&content) + .ok() + .and_then(|b| b["redacts"].as_str().and_then(|s| EventId::parse(s).ok())) + { + return Ok(SendEventResponse::from_event_id( + self.room.redact(&redacts, None, None).await?.event_id, + )); + } Ok(match (state_key, delayed_event_parameters) { (None, None) => SendEventResponse::from_event_id( self.room.send_raw(&type_str, content).await?.event_id, diff --git a/crates/matrix-sdk/tests/integration/widget.rs b/crates/matrix-sdk/tests/integration/widget.rs index 72d29fcbd..909880dcd 100644 --- a/crates/matrix-sdk/tests/integration/widget.rs +++ b/crates/matrix-sdk/tests/integration/widget.rs @@ -19,6 +19,7 @@ use async_trait::async_trait; use futures_util::FutureExt; use matrix_sdk::{ config::SyncSettings, + test_utils::mocks::MatrixMockServer, widget::{ Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings, }, @@ -26,11 +27,11 @@ use matrix_sdk::{ }; use matrix_sdk_common::{executor::spawn, timeout::timeout}; use matrix_sdk_test::{ - async_test, mocks::mock_encryption_state, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, - ALICE, BOB, + async_test, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, }; use once_cell::sync::Lazy; use ruma::{ + event_id, events::room::{ member::{MembershipState, RoomMemberEventContent}, message::RoomMessageEventContent, @@ -46,10 +47,10 @@ use serde_json::{json, Value as JsonValue}; use tracing::error; use wiremock::{ matchers::{header, method, path_regex, query_param}, - Mock, MockServer, ResponseTemplate, + Mock, ResponseTemplate, }; -use crate::{logged_in_client_with_server, mock_sync}; +use crate::mock_sync; /// Create a JSON string from a [`json!`][serde_json::json] "literal". #[macro_export] @@ -60,7 +61,9 @@ macro_rules! json_string { const WIDGET_ID: &str = "test-widget"; static ROOM_ID: Lazy = Lazy::new(|| owned_room_id!("!a98sd12bjh:example.org")); -async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, WidgetDriverHandle) { +async fn run_test_driver( + init_on_content_load: bool, +) -> (Client, MatrixMockServer, WidgetDriverHandle) { struct DummyCapabilitiesProvider; #[async_trait] @@ -70,20 +73,11 @@ async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, Wid capabilities } } + let mock_server = MatrixMockServer::new().await; + let client = mock_server.make_client().await; - let (client, mock_server) = logged_in_client_with_server().await; - let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000)); - - let mut sync_builder = SyncResponseBuilder::new(); - sync_builder.add_joined_room(JoinedRoomBuilder::new(&ROOM_ID)); - - mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await; - let _response = client.sync_once(sync_settings.clone()).await.unwrap(); - mock_server.reset().await; - - mock_encryption_state(&mock_server, false).await; - - let room = client.get_room(&ROOM_ID).unwrap(); + let room = mock_server.sync_joined_room(&client, &ROOM_ID).await; + mock_server.mock_room_state_encryption().plain().mount().await; let (driver, handle) = WidgetDriver::new( WidgetSettings::new(WIDGET_ID.to_owned(), init_on_content_load, "https://foo.bar/widget") @@ -257,7 +251,7 @@ async fn test_read_messages() { .and(query_param("limit", "2")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; // Ask the driver to read messages @@ -282,8 +276,6 @@ async fn test_read_messages() { let first_event = &events[0]; assert_eq!(first_event["content"]["body"], "hello"); } - - mock_server.verify().await; } #[async_test] @@ -354,7 +346,7 @@ async fn test_read_messages_with_msgtype_capabilities() { .and(query_param("limit", "3")) .respond_with(ResponseTemplate::new(200).set_body_json(response_json)) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; // Ask the driver to read messages @@ -379,8 +371,6 @@ async fn test_read_messages_with_msgtype_capabilities() { let first_event = &events[0]; assert_eq!(first_event["content"]["body"], "hello"); } - - mock_server.verify().await; } #[async_test] @@ -488,7 +478,7 @@ async fn test_receive_live_events() { )), ); - mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await; + mock_sync(mock_server.server(), sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(SyncSettings::new().timeout(Duration::from_millis(3000))).await.unwrap(); @@ -531,7 +521,7 @@ async fn test_send_room_message() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/send/m.room.message/.*$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -556,7 +546,6 @@ async fn test_send_room_message() { assert_eq!(event_id, "$foobar"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -573,7 +562,7 @@ async fn test_send_room_name() { .and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.name/?$")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "event_id": "$foobar" }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -598,7 +587,6 @@ async fn test_send_room_name() { assert_eq!(event_id, "$foobar"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -620,7 +608,7 @@ async fn test_send_delayed_message_event() { "delay_id": "1234", }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -646,7 +634,6 @@ async fn test_send_delayed_message_event() { assert_eq!(delay_id, "1234"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -668,7 +655,7 @@ async fn test_send_delayed_state_event() { "delay_id": "1234", }))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -694,7 +681,6 @@ async fn test_send_delayed_state_event() { assert_eq!(delay_id, "1234"); // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -744,7 +730,7 @@ async fn test_update_delayed_event() { .and(path_regex(r"^/_matrix/client/unstable/org.matrix.msc4140/delayed_events/1234")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({}))) .expect(1) - .mount(&mock_server) + .mount(mock_server.server()) .await; send_request( @@ -764,9 +750,6 @@ async fn test_update_delayed_event() { assert_eq!(response["action"], "org.matrix.msc4157.update_delayed_event"); let empty_response = response["response"].clone(); assert_eq!(empty_response, serde_json::from_str::("{}").unwrap()); - - // Make sure the event-sending endpoint was hit exactly once - mock_server.verify().await; } #[async_test] @@ -827,6 +810,45 @@ async fn test_try_update_delayed_event_without_permission_negotiate() { } } +#[async_test] +async fn test_send_redaction() { + let (_, mock_server, driver_handle) = run_test_driver(false).await; + + negotiate_capabilities( + &driver_handle, + json!([ + // "org.matrix.msc4157.send.delayed_event", + "org.matrix.msc2762.send.event:m.room.redaction" + ]), + ) + .await; + + mock_server.mock_room_redact().ok(event_id!("$redact_event_id")).mock_once().mount().await; + + send_request( + &driver_handle, + "send-redact-message", + "send_event", + json!({ + "type": "m.room.redaction", + "content": { + "redacts": "$1234" + }, + }), + ) + .await; + + // Receive the response + let msg = recv_message(&driver_handle).await; + assert_eq!(msg["api"], "fromWidget"); + assert_eq!(msg["action"], "send_event"); + let redact_event_id = msg["response"]["event_id"].as_str().unwrap(); + let redact_room_id = msg["response"]["room_id"].as_str().unwrap(); + + assert_eq!(redact_event_id, "$redact_event_id"); + assert_eq!(redact_room_id, "!a98sd12bjh:example.org"); +} + async fn negotiate_capabilities(driver_handle: &WidgetDriverHandle, caps: JsonValue) { { // Receive toWidget capabilities request