Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(WidgetDriver): Pass Matrix API errors to the widget #4241

Merged
merged 17 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions crates/matrix-sdk/src/test_utils/mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use ruma::{
directory::PublicRoomsChunk,
events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
time::Duration,
MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName,
};
use serde::Deserialize;
Expand Down Expand Up @@ -952,6 +953,72 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> {
}
}

/// Ensures the event was sent as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_message_event, DelayParameters},
/// events::{message::MessageEventContent, AnyMessageLikeEventContent},
/// room_id,
/// time::Duration,
/// TransactionId,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use serde_json::json;
/// use wiremock::ResponseTemplate;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked =
/// room.send_raw("m.room.message", json!({ "body": "Hello world" })).await;
///
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_message_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// TransactionId::new(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyMessageLikeEventContent::Message(MessageEventContent::plain("hello world")),
/// )
/// .unwrap();
///
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

/// Returns a send endpoint that emulates success, i.e. the event has been
/// sent with the given event id.
///
Expand Down Expand Up @@ -1117,6 +1184,69 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> {
Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self }
}

/// Ensures the event was sent as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_state_event, DelayParameters},
/// events::{room::create::RoomCreateEventContent, AnyStateEventContent},
/// room_id,
/// time::Duration,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use wiremock::ResponseTemplate;
/// use serde_json::json;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send_state()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await;
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_state_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// "".to_owned(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyStateEventContent::RoomCreate(RoomCreateEventContent::new_v11()),
/// )
/// .unwrap();
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
///
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

///
/// ```
/// # tokio_test::block_on(async {
Expand Down
4 changes: 3 additions & 1 deletion crates/matrix-sdk/src/widget/machine/driver_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ where
Self { request_meta: None, _phantom: PhantomData }
}

/// Setup a callback function that will be called once the matrix driver has
/// processed the request.
pub(crate) fn then(
self,
response_handler: impl FnOnce(Result<T, String>, &mut WidgetMachine) -> Vec<Action>
response_handler: impl FnOnce(Result<T, crate::Error>, &mut WidgetMachine) -> Vec<Action>
+ Send
+ 'static,
) {
Expand Down
71 changes: 64 additions & 7 deletions crates/matrix-sdk/src/widget/machine/from_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt;

use as_variant::as_variant;
use ruma::{
api::client::delayed_events::{
delayed_message_event, delayed_state_event, update_delayed_event,
api::client::{
delayed_events::{delayed_message_event, delayed_state_event, update_delayed_event},
error::{ErrorBody, StandardErrorBody},
},
events::{AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
Expand All @@ -25,7 +25,7 @@ use ruma::{
use serde::{Deserialize, Serialize};

use super::{SendEventRequest, UpdateDelayedEventRequest};
use crate::widget::StateKeySelector;
use crate::{widget::StateKeySelector, Error, HttpError, RumaApiError};

#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case", content = "data")]
Expand All @@ -41,28 +41,85 @@ pub(super) enum FromWidgetRequest {
DelayedEventUpdate(UpdateDelayedEventRequest),
}

/// The full response a client sends to a [`FromWidgetRequest`] in case of an
/// error.
#[derive(Serialize)]
pub(super) struct FromWidgetErrorResponse {
error: FromWidgetError,
}

impl FromWidgetErrorResponse {
pub(super) fn new(e: impl fmt::Display) -> Self {
Self { error: FromWidgetError { message: e.to_string() } }
/// Create a error response to send to the widget from an http error.
pub(crate) fn from_http_error(error: HttpError) -> Self {
let message = error.to_string();
let matrix_api_error = as_variant!(error, HttpError::Api(ruma::api::error::FromHttpResponseError::Server(RumaApiError::ClientApi(err))) => err);

Self {
error: FromWidgetError {
message,
matrix_api_error: matrix_api_error.and_then(|api_error| match api_error.body {
ErrorBody::Standard { kind, message } => Some(FromWidgetMatrixErrorBody {
http_status: api_error.status_code.as_u16().into(),
response: StandardErrorBody { kind, message },
}),
_ => None,
}),
},
}
}

/// Create a error response to send to the widget from a matrix sdk error.
pub(crate) fn from_error(error: Error) -> Self {
match error {
Error::Http(e) => FromWidgetErrorResponse::from_http_error(e),
// For UnknownError's we do not want to have the `unknown error` bit in the message.
// Hence we only convert the inner error to a string.
Error::UnknownError(e) => FromWidgetErrorResponse::from_string(e.to_string()),
_ => FromWidgetErrorResponse::from_string(error.to_string()),
}
}

/// Create a error response to send to the widget from a string.
pub(crate) fn from_string<S: Into<String>>(error: S) -> Self {
Self { error: FromWidgetError { message: error.into(), matrix_api_error: None } }
}
}

/// Serializable section of an error response send by the client as a
/// response to a [`FromWidgetRequest`].
#[derive(Serialize)]
struct FromWidgetError {
/// Unspecified error message text that caused this widget action to
/// fail.
///
/// This is useful to prompt the user on an issue but cannot be used to
/// decide on how to deal with the error.
message: String,

/// Optional matrix error hinting at workarounds for specific errors.
matrix_api_error: Option<FromWidgetMatrixErrorBody>,
}

/// Serializable section of a widget response that represents a matrix error.
#[derive(Serialize)]
struct FromWidgetMatrixErrorBody {
/// Status code of the http response.
http_status: u32,

/// Standard error response including the `errorcode` and the `error`
/// message as defined in the [spec](https://spec.matrix.org/v1.12/client-server-api/#standard-error-response).
response: StandardErrorBody,
}

/// The serializable section of a widget response containing the supported
/// versions.
#[derive(Serialize)]
pub(super) struct SupportedApiVersionsResponse {
supported_versions: Vec<ApiVersion>,
}

impl SupportedApiVersionsResponse {
/// The currently supported widget api versions from the rust widget driver.
pub(super) fn new() -> Self {
Self {
supported_versions: vec![
Expand Down
7 changes: 5 additions & 2 deletions crates/matrix-sdk/src/widget/machine/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ pub(crate) enum IncomingMessage {
/// The ID of the request that this response corresponds to.
request_id: Uuid,

/// The result of the request: response data or error message.
response: Result<MatrixDriverResponse, String>,
/// Result of the request: the response data, or a matrix sdk error.
///
/// Http errors will be forwarded to the widget in a specified format so
/// the widget can parse the error.
response: Result<MatrixDriverResponse, crate::Error>,
},

/// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event.
Expand Down
Loading
Loading