widget: Add toWidget & MatrixDriver response handling skeleton

This commit is contained in:
Jonas Platte
2023-10-17 18:01:38 +02:00
committed by Jonas Platte
parent f75b2cd1d0
commit e038ced2c6
9 changed files with 360 additions and 167 deletions

1
Cargo.lock generated
View File

@@ -3070,6 +3070,7 @@ dependencies = [
"hyper",
"image 0.24.7",
"imbl",
"indexmap 2.0.2",
"language-tags",
"mas-oidc-client",
"matrix-sdk-base",

View File

@@ -81,6 +81,7 @@ futures-util = { workspace = true }
http = { workspace = true }
hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true }
imbl = { version = "2.0.0", features = ["serde"] }
indexmap = "2.0.2"
language-tags = { version = "0.3.2", optional = true }
matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }

View File

@@ -12,36 +12,45 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{borrow::Cow, error::Error, ops::Deref};
use std::error::Error;
use ruma::events::{MessageLikeEventType, StateEventType, TimelineEventType};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use crate::widget::{Permissions, StateKeySelector};
use super::driver_req::AcquirePermissions;
use crate::widget::StateKeySelector;
/// Action (a command) that client (driver) must perform.
#[allow(dead_code)] // TODO: Remove once all actions are implemented.
#[derive(Debug)]
pub(crate) enum Action {
/// Send a raw message to the widget.
SendToWidget(String),
/// Acquire permissions from the user given the set of desired permissions.
/// Must eventually be answered with `Event::PermissionsAcquired`.
AcquirePermissions(Command<Permissions>),
/// Get OpenId token for a given request ID.
GetOpenId(Command<()>),
/// Read message event(s).
ReadMessageLikeEvent(Command<ReadMessageLikeEventCommand>),
/// Read state event(s).
ReadStateEvent(Command<ReadStateEventCommand>),
/// Send matrix event that corresponds to the given description.
SendMatrixEvent(Command<SendEventCommand>),
/// Command that is sent from the client widget API state machine to the
/// client (driver) that must be performed. Once the command is executed,
/// the client will typically generate an `Event` with the result of it.
MatrixDriverRequest {
/// Certain commands are typically answered with certain event once the
/// command is performed. The api state machine will "tag" each command
/// with some "cookie" (in this case just an ID), so that once the
/// result of the execution of this command is received, it could be
/// matched.
request_id: Uuid,
/// Data associated with this command.
data: MatrixDriverRequestData,
},
/// Subscribe to the events in the *current* room, i.e. a room which this
/// widget is instantiated with. The client is aware of the room.
#[allow(dead_code)]
Subscribe,
/// Unsuscribe from the events in the *current* room. Symmetrical to
/// `Subscribe`.
#[allow(dead_code)]
Unsubscribe,
}
@@ -78,38 +87,24 @@ pub(crate) struct SendEventCommand {
pub(crate) content: JsonValue,
}
/// Command that is sent from the client widget API state machine to the
/// client (driver) that must be performed. Once the command is executed,
/// the client will typically generate an `Event` with the result of it.
#[derive(Debug)]
pub(crate) struct Command<T> {
/// Certain commands are typically answered with certain event once the
/// command is performed. The api state machine will "tag" each command
/// with some "cookie" (in this case just an ID), so that once the
/// result of the execution of this command is received, it could be
/// matched.
id: String,
// Data associated with this command.
data: T,
}
#[allow(dead_code)]
pub(crate) enum MatrixDriverRequestData {
/// Acquire permissions from the user given the set of desired permissions.
/// Must eventually be answered with `Event::PermissionsAcquired`.
AcquirePermissions(AcquirePermissions),
impl<T> Command<T> {
/// Consumes the command and produces a command result with given data.
pub(crate) fn result<U, E: Error>(self, result: Result<U, E>) -> CommandResult<U> {
CommandResult { id: self.id, result: result.map_err(|e| e.to_string().into()) }
}
/// Get OpenId token for a given request ID.
GetOpenId,
pub(crate) fn ok<U>(self, value: U) -> CommandResult<U> {
CommandResult { id: self.id, result: Ok(value) }
}
}
/// Read message event(s).
ReadMessageLikeEvent(ReadMessageLikeEventCommand),
impl<T> Deref for Command<T> {
type Target = T;
/// Read state event(s).
ReadStateEvent(ReadStateEventCommand),
fn deref(&self) -> &Self::Target {
&self.data
}
/// Send matrix event that corresponds to the given description.
SendMatrixEvent(SendEventCommand),
}
/// The result of the execution of a command. Note that this type can only be
@@ -118,9 +113,19 @@ impl<T> Deref for Command<T> {
/// client (driver) won't be able to send "invalid" commands, because they could
/// only be generated from a `Command` instance.
#[allow(dead_code)] // TODO: Remove once results are used.
pub(crate) struct CommandResult<T> {
pub(crate) struct MatrixDriverResponse<T> {
/// ID of the command that was executed. See `Command::id` for more details.
id: String,
request_id: Uuid,
/// Result of the execution of the command.
result: Result<T, Cow<'static, str>>,
result: Result<T, String>,
}
impl<T> MatrixDriverResponse<T> {
pub(crate) fn new<E: Error>(request_id: Uuid, result: Result<T, E>) -> Self {
Self { request_id, result: result.map_err(|e| e.to_string()) }
}
pub(crate) fn ok(request_id: Uuid, value: T) -> Self {
Self { request_id, result: Ok(value) }
}
}

View File

@@ -0,0 +1,134 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#![allow(dead_code)]
//! A high-level API for requests that we send to the matrix driver.
use std::marker::PhantomData;
//use ruma::{
// api::client::account::request_openid_token::v3::Response as
// RumaOpenIdResponse, events::AnyTimelineEvent, serde::Raw, OwnedEventId,
//};
use tracing::error;
use super::{
actions::MatrixDriverRequestData,
//actions::{ReadMessageLikeEventCommand, SendEventCommand},
Event,
MatrixDriverRequestMeta,
WidgetMachine,
};
use crate::widget::Permissions;
/// A handle to a pending `toWidget` request.
pub(crate) struct MatrixDriverRequestHandle<'m, T> {
request_meta: Option<&'m mut MatrixDriverRequestMeta>,
_phantom: PhantomData<fn() -> T>,
}
impl<'m, T> MatrixDriverRequestHandle<'m, T>
where
T: MatrixDriverResponse,
{
pub(crate) fn new(request_meta: &'m mut MatrixDriverRequestMeta) -> Self {
Self { request_meta: Some(request_meta), _phantom: PhantomData }
}
pub(crate) fn null() -> Self {
Self { request_meta: None, _phantom: PhantomData }
}
pub(crate) fn then(
self,
response_handler: impl FnOnce(T, &mut WidgetMachine) + Send + 'static,
) {
if let Some(request_meta) = self.request_meta {
request_meta.response_fn = Some(Box::new(move |event, machine| {
if let Some(response_data) = T::from_event(event) {
response_handler(response_data, machine)
}
}));
}
}
}
/// Represents a request that the widget API state machine can send.
pub(crate) trait MatrixDriverRequest: Into<MatrixDriverRequestData> {
type Response: MatrixDriverResponse;
}
pub(crate) trait MatrixDriverResponse: Sized {
fn from_event(_: Event) -> Option<Self>;
}
/// Ask the client (permission provider) to acquire given permissions
/// from the user. The client must eventually respond with granted permissions.
#[derive(Debug)]
pub(crate) struct AcquirePermissions {
pub(crate) desired_permissions: Permissions,
}
pub(crate) struct AcquirePermissionsResponse {
pub(crate) granted_permissions: Permissions,
}
impl From<AcquirePermissions> for MatrixDriverRequestData {
fn from(value: AcquirePermissions) -> Self {
MatrixDriverRequestData::AcquirePermissions(value)
}
}
impl MatrixDriverRequest for AcquirePermissions {
type Response = AcquirePermissionsResponse;
}
impl MatrixDriverResponse for AcquirePermissionsResponse {
fn from_event(ev: Event) -> Option<Self> {
match ev {
Event::PermissionsAcquired(_) => todo!(),
Event::MessageFromWidget(_) | Event::MatrixEventReceived(_) => {
error!("this should be unreachable, no ID to match");
None
}
Event::OpenIdReceived(_) | Event::MatrixEventSent(_) | Event::MatrixEventRead(_) => {
error!("bug in MatrixDriver, received wrong event response");
None
}
}
}
}
/*
/// Request open ID from the Matrix client.
pub(crate) struct RequestOpenId;
impl MatrixDriverRequest for RequestOpenId {
type Response = RumaOpenIdResponse;
}
/// Ask the client to read matrix event(s) that corresponds to the given
/// description and return a list of events as a response.
pub(crate) struct ReadMatrixEvent(pub(crate) ReadMessageLikeEventCommand);
impl MatrixDriverRequest for ReadMatrixEvent {
type Response = Vec<Raw<AnyTimelineEvent>>;
}
/// Ask the client to send matrix event that corresponds to the given
/// description and return an event ID as a response.
pub(crate) struct SendMatrixEvent(pub(crate) SendEventCommand);
impl MatrixDriverRequest for SendMatrixEvent {
type Response = OwnedEventId;
}
*/

View File

@@ -17,7 +17,7 @@ use ruma::{
events::AnyTimelineEvent, serde::Raw, OwnedEventId,
};
use super::actions::CommandResult;
use super::actions::MatrixDriverResponse;
use crate::widget::Permissions;
/// Incoming event that the client API must process.
@@ -29,14 +29,14 @@ pub(crate) enum Event {
MatrixEventReceived(Raw<AnyTimelineEvent>),
/// Client acquired permissions from the user.
/// A response to an `Action::AcquirePermissions` command.
PermissionsAcquired(CommandResult<Permissions>),
PermissionsAcquired(MatrixDriverResponse<Permissions>),
/// Client got OpenId token for a given request ID.
/// A response to an `Action::GetOpenId` command.
OpenIdReceived(CommandResult<RumaOpenIdResponse>),
OpenIdReceived(MatrixDriverResponse<RumaOpenIdResponse>),
/// Client read some matrix event(s).
/// A response to an `Action::ReadMatrixEvent` commands.
MatrixEventRead(CommandResult<Vec<Raw<AnyTimelineEvent>>>),
MatrixEventRead(MatrixDriverResponse<Vec<Raw<AnyTimelineEvent>>>),
/// Client sent some matrix event. The response contains the event ID.
/// A response to an `Action::SendMatrixEvent` command.
MatrixEventSent(CommandResult<OwnedEventId>),
MatrixEventSent(MatrixDriverResponse<OwnedEventId>),
}

View File

@@ -16,23 +16,28 @@
#![warn(unreachable_pub)]
use indexmap::{map::Entry, IndexMap};
use serde::Serialize;
use serde_json::value::RawValue as RawJsonValue;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use tracing::{error, instrument};
use uuid::Uuid;
use self::to_widget::{RequestPermissions, ToWidgetRequest};
use self::{
driver_req::{AcquirePermissions, MatrixDriverRequest, MatrixDriverRequestHandle},
to_widget::{RequestPermissions, ToWidgetRequest, ToWidgetRequestHandle},
};
mod actions;
mod driver_req;
mod events;
mod openid;
mod outgoing;
#[cfg(test)]
mod tests;
mod to_widget;
pub(crate) use self::{
actions::{Action, SendEventCommand},
actions::{Action, MatrixDriverRequestData, MatrixDriverResponse, SendEventCommand},
events::Event,
};
#[cfg(doc)]
@@ -44,6 +49,8 @@ use super::WidgetDriver;
pub(crate) struct WidgetMachine {
widget_id: String,
actions_sender: UnboundedSender<Action>,
pending_to_widget_requests: IndexMap<Uuid, ToWidgetRequestMeta>,
pending_matrix_driver_requests: IndexMap<Uuid, MatrixDriverRequestMeta>,
}
impl WidgetMachine {
@@ -55,17 +62,31 @@ impl WidgetMachine {
init_on_content_load: bool,
) -> (Self, UnboundedReceiver<Action>) {
let (actions_sender, actions_receiver) = unbounded_channel();
let this = Self { widget_id, actions_sender };
let mut machine = Self {
widget_id,
actions_sender,
pending_to_widget_requests: IndexMap::new(),
pending_matrix_driver_requests: IndexMap::new(),
};
if !init_on_content_load {
this.send_to_widget(RequestPermissions {});
machine
.send_to_widget_request(RequestPermissions {})
// rustfmt please
.then(|desired_permissions, machine| {
machine.send_matrix_driver_request(AcquirePermissions { desired_permissions });
// TODO: use the result! (chain another `.then`)
});
}
(this, actions_receiver)
(machine, actions_receiver)
}
#[instrument(skip_all, fields(action = T::ACTION))]
fn send_to_widget<T: ToWidgetRequest>(&self, to_widget_request: T) {
fn send_to_widget_request<T: ToWidgetRequest>(
&mut self,
to_widget_request: T,
) -> ToWidgetRequestHandle<'_, T::ResponseData> {
#[derive(Serialize)]
#[serde(tag = "api", rename = "toWidget", rename_all = "camelCase")]
struct ToWidgetRequestSerHelper<'a, T> {
@@ -75,23 +96,57 @@ impl WidgetMachine {
data: T,
}
let request_id = Uuid::new_v4();
let full_request = ToWidgetRequestSerHelper {
widget_id: &self.widget_id,
request_id: Uuid::new_v4(),
request_id,
action: T::ACTION,
data: to_widget_request,
};
let serialized = match serde_json::to_string(&full_request) {
Ok(msg) => msg,
Err(e) => {
error!("Failed to serialize outgoing message: {e}");
return;
return ToWidgetRequestHandle::null();
}
};
if let Err(e) = self.actions_sender.send(Action::SendToWidget(serialized)) {
error!("Failed to send action: {e}");
return ToWidgetRequestHandle::null();
}
let request_meta = ToWidgetRequestMeta::new(T::ACTION);
let Entry::Vacant(entry) = self.pending_to_widget_requests.entry(request_id) else {
panic!("uuid collision");
};
let meta = entry.insert(request_meta);
ToWidgetRequestHandle::new(meta)
}
#[instrument(skip_all)]
fn send_matrix_driver_request<T: MatrixDriverRequest>(
&mut self,
matrix_driver_request: T,
) -> MatrixDriverRequestHandle<'_, T::Response> {
let request_id = Uuid::new_v4();
if let Err(e) = self
.actions_sender
.send(Action::MatrixDriverRequest { request_id, data: matrix_driver_request.into() })
{
error!("Failed to send action: {e}");
return MatrixDriverRequestHandle::null();
}
let request_meta = MatrixDriverRequestMeta::new();
let Entry::Vacant(entry) = self.pending_matrix_driver_requests.entry(request_id) else {
panic!("uuid collision");
};
let meta = entry.insert(request_meta);
MatrixDriverRequestHandle::new(meta)
}
/// Processes an incoming event (an incoming raw message from a widget,
@@ -101,3 +156,29 @@ impl WidgetMachine {
// TODO: Process the event.
}
}
type ToWidgetResponseFn = Box<dyn FnOnce(Box<RawJsonValue>, &mut WidgetMachine) + Send>;
pub(crate) struct ToWidgetRequestMeta {
#[allow(dead_code)]
action: &'static str,
response_fn: Option<ToWidgetResponseFn>,
}
impl ToWidgetRequestMeta {
fn new(action: &'static str) -> Self {
Self { action, response_fn: None }
}
}
type MatrixDriverResponseFn = Box<dyn FnOnce(Event, &mut WidgetMachine) + Send>;
pub(crate) struct MatrixDriverRequestMeta {
response_fn: Option<MatrixDriverResponseFn>,
}
impl MatrixDriverRequestMeta {
fn new() -> Self {
Self { response_fn: None }
}
}

View File

@@ -1,82 +0,0 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! A high-level API to generate commands (requests) that we send either
//! directly to the widget, or to the matrix driver/client.
use ruma::{
api::client::account::request_openid_token::v3::Response as RumaOpenIdResponse,
events::AnyTimelineEvent, serde::Raw, OwnedEventId,
};
use super::{
actions::{ReadMessageLikeEventCommand, SendEventCommand},
openid::OpenIdResponse,
};
use crate::widget::Permissions;
/// Represents a request that the widget API state machine can send.
pub(crate) trait Request {
type Response;
}
// `toWidget` requests
/// Send a request to a widget asking it to respond with the list of
/// permissinos (capabilities) that the widget wants to have.
pub(crate) struct RequestPermissions;
impl Request for RequestPermissions {
type Response = Permissions;
}
/// Send a request to the widget asking it to update its permissions.
pub(crate) struct UpdatePermissions(pub(crate) Permissions);
impl Request for UpdatePermissions {
type Response = ();
}
/// Send a request to the widget asking it to update its open ID state.
pub(crate) struct UpdateOpenId(pub(crate) OpenIdResponse);
impl Request for UpdateOpenId {
type Response = ();
}
// requests to the `MatrixDriver`
/// Ask the client (permission provider) to acquire given permissions
/// from the user. The client must eventually respond with granted permissions.
pub(crate) struct AcquirePermissions(pub(crate) Permissions);
impl Request for AcquirePermissions {
type Response = Permissions;
}
/// Request open ID from the Matrix client.
pub(crate) struct RequestOpenId;
impl Request for RequestOpenId {
type Response = RumaOpenIdResponse;
}
/// Ask the client to read matrix event(s) that corresponds to the given
/// description and return a list of events as a response.
pub(crate) struct ReadMatrixEvent(pub(crate) ReadMessageLikeEventCommand);
impl Request for ReadMatrixEvent {
type Response = Vec<Raw<AnyTimelineEvent>>;
}
/// Ask the client to send matrix event that corresponds to the given
/// description and return an event ID as a response.
pub(crate) struct SendMatrixEvent(pub(crate) SendEventCommand);
impl Request for SendMatrixEvent {
type Response = OwnedEventId;
}

View File

@@ -12,13 +12,53 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use serde::Serialize;
use std::marker::PhantomData;
use serde::{de::DeserializeOwned, Serialize};
use tracing::error;
use super::{ToWidgetRequestMeta, WidgetMachine};
use crate::widget::Permissions;
/// A handle to a pending `toWidget` request.
pub(crate) struct ToWidgetRequestHandle<'m, T> {
request_meta: Option<&'m mut ToWidgetRequestMeta>,
_phantom: PhantomData<fn() -> T>,
}
impl<'m, T> ToWidgetRequestHandle<'m, T>
where
T: DeserializeOwned,
{
pub(crate) fn new(request_meta: &'m mut ToWidgetRequestMeta) -> Self {
Self { request_meta: Some(request_meta), _phantom: PhantomData }
}
pub(crate) fn null() -> Self {
Self { request_meta: None, _phantom: PhantomData }
}
pub(crate) fn then(
self,
response_handler: impl FnOnce(T, &mut WidgetMachine) + Send + 'static,
) {
if let Some(request_meta) = self.request_meta {
request_meta.response_fn = Some(Box::new(move |raw_response_data, machine| {
match serde_json::from_str(raw_response_data.get()) {
Ok(response_data) => response_handler(response_data, machine),
Err(e) => error!("Failed to deserialize toWidget response: {e}"),
}
}));
}
}
}
/// A request that the driver can send to the widget.
///
/// In postmessage interface terms: an `"api": "toWidget"` message.
pub(crate) trait ToWidgetRequest: Serialize {
const ACTION: &'static str;
type ResponseData: DeserializeOwned;
}
/// Request the widget to send the list of capabilities that it wants to have.
@@ -27,4 +67,5 @@ pub(crate) struct RequestPermissions {}
impl ToWidgetRequest for RequestPermissions {
const ACTION: &'static str = "capabilities";
type ResponseData = Permissions;
}

View File

@@ -19,7 +19,10 @@ use tokio::sync::mpsc::unbounded_channel;
use tokio_util::sync::{CancellationToken, DropGuard};
use self::{
machine::{Action, Event, SendEventCommand, WidgetMachine},
machine::{
Action, Event, MatrixDriverRequestData, MatrixDriverResponse, SendEventCommand,
WidgetMachine,
},
matrix::MatrixDriver,
};
use crate::{room::Room, Result};
@@ -148,31 +151,40 @@ impl WidgetDriver {
while let Some(action) = actions.recv().await {
match action {
Action::SendToWidget(msg) => self.to_widget_tx.send(msg).await.map_err(|_| ())?,
Action::AcquirePermissions(cmd) => {
let obtained = permissions_provider.acquire_permissions(cmd.clone()).await;
let event = Event::PermissionsAcquired(cmd.ok(obtained));
events_tx.send(event).map_err(|_| ())?;
}
Action::GetOpenId(cmd) => {
let result = cmd.result(matrix_driver.get_open_id().await);
events_tx.send(Event::OpenIdReceived(result)).map_err(|_| ())?;
}
Action::ReadMessageLikeEvent(cmd) => {
let events = matrix_driver
.read_message_like_events(cmd.event_type.clone(), cmd.limit)
.await;
events_tx.send(Event::MatrixEventRead(cmd.result(events))).map_err(|_| ())?;
}
Action::ReadStateEvent(cmd) => {
let events = matrix_driver
.read_state_events(cmd.event_type.clone(), &cmd.state_key)
.await;
events_tx.send(Event::MatrixEventRead(cmd.result(events))).map_err(|_| ())?;
}
Action::SendMatrixEvent(cmd) => {
let SendEventCommand { event_type, state_key, content } = cmd.clone();
let matrix_event_id = matrix_driver.send(event_type, state_key, content).await;
let event = Event::MatrixEventSent(cmd.result(matrix_event_id));
Action::MatrixDriverRequest { request_id: id, data } => {
let event = match data {
MatrixDriverRequestData::AcquirePermissions(cmd) => {
let obtained = permissions_provider
.acquire_permissions(cmd.desired_permissions.clone())
.await;
Event::PermissionsAcquired(MatrixDriverResponse::ok(id, obtained))
}
MatrixDriverRequestData::GetOpenId => {
let result =
MatrixDriverResponse::new(id, matrix_driver.get_open_id().await);
Event::OpenIdReceived(result)
}
MatrixDriverRequestData::ReadMessageLikeEvent(cmd) => {
let events = matrix_driver
.read_message_like_events(cmd.event_type.clone(), cmd.limit)
.await;
Event::MatrixEventRead(MatrixDriverResponse::new(id, events))
}
MatrixDriverRequestData::ReadStateEvent(cmd) => {
let events = matrix_driver
.read_state_events(cmd.event_type.clone(), &cmd.state_key)
.await;
Event::MatrixEventRead(MatrixDriverResponse::new(id, events))
}
MatrixDriverRequestData::SendMatrixEvent(cmd) => {
let SendEventCommand { event_type, state_key, content } = cmd.clone();
let matrix_event_id =
matrix_driver.send(event_type, state_key, content).await;
Event::MatrixEventSent(MatrixDriverResponse::new(id, matrix_event_id))
}
};
events_tx.send(event).map_err(|_| ())?;
}
Action::Subscribe => {