629 lines
21 KiB
Rust
629 lines
21 KiB
Rust
// 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.
|
|
|
|
use std::{pin::pin, time::Duration};
|
|
|
|
use assert_matches::assert_matches;
|
|
use async_trait::async_trait;
|
|
use futures_util::FutureExt;
|
|
use matrix_sdk::{
|
|
config::SyncSettings,
|
|
widget::{
|
|
Capabilities, CapabilitiesProvider, WidgetDriver, WidgetDriverHandle, WidgetSettings,
|
|
},
|
|
Client,
|
|
};
|
|
use matrix_sdk_common::{executor::spawn, timeout::timeout};
|
|
use matrix_sdk_test::{
|
|
async_test, EventBuilder, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB,
|
|
};
|
|
use once_cell::sync::Lazy;
|
|
use ruma::{
|
|
events::room::{
|
|
member::{MembershipState, RoomMemberEventContent},
|
|
message::RoomMessageEventContent,
|
|
name::RoomNameEventContent,
|
|
topic::RoomTopicEventContent,
|
|
},
|
|
owned_room_id,
|
|
serde::JsonObject,
|
|
user_id, OwnedRoomId,
|
|
};
|
|
use serde::Serialize;
|
|
use serde_json::{json, Value as JsonValue};
|
|
use tracing::error;
|
|
use wiremock::{
|
|
matchers::{header, method, path_regex, query_param},
|
|
Mock, MockServer, ResponseTemplate,
|
|
};
|
|
|
|
use crate::{logged_in_client_with_server, mock_encryption_state, mock_sync};
|
|
|
|
/// Create a JSON string from a [`json!`][serde_json::json] "literal".
|
|
#[macro_export]
|
|
macro_rules! json_string {
|
|
($( $tt:tt )*) => { ::serde_json::json!( $($tt)* ).to_string() };
|
|
}
|
|
|
|
const WIDGET_ID: &str = "test-widget";
|
|
static ROOM_ID: Lazy<OwnedRoomId> = Lazy::new(|| owned_room_id!("!a98sd12bjh:example.org"));
|
|
|
|
async fn run_test_driver(init_on_content_load: bool) -> (Client, MockServer, WidgetDriverHandle) {
|
|
struct DummyCapabilitiesProvider;
|
|
|
|
#[async_trait]
|
|
impl CapabilitiesProvider for DummyCapabilitiesProvider {
|
|
async fn acquire_capabilities(&self, capabilities: Capabilities) -> Capabilities {
|
|
// Grant all capabilities that the widget asks for
|
|
capabilities
|
|
}
|
|
}
|
|
|
|
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 (driver, handle) = WidgetDriver::new(
|
|
WidgetSettings::new(WIDGET_ID.to_owned(), init_on_content_load, "https://foo.bar/widget")
|
|
.unwrap(),
|
|
);
|
|
|
|
spawn(async move {
|
|
if let Err(()) = driver.run(room, DummyCapabilitiesProvider).await {
|
|
error!("An error encountered in running the WidgetDriver (no details available yet)");
|
|
}
|
|
});
|
|
|
|
(client, mock_server, handle)
|
|
}
|
|
|
|
async fn recv_message(driver_handle: &WidgetDriverHandle) -> JsonObject {
|
|
let fut = pin!(driver_handle.recv());
|
|
let msg = timeout(fut, Duration::from_secs(1)).await.unwrap();
|
|
serde_json::from_str(&msg.unwrap()).unwrap()
|
|
}
|
|
|
|
async fn send_request(
|
|
driver_handle: &WidgetDriverHandle,
|
|
request_id: &str,
|
|
action: &str,
|
|
data: impl Serialize,
|
|
) {
|
|
let sent = driver_handle
|
|
.send(json_string!({
|
|
"api": "fromWidget",
|
|
"widgetId": WIDGET_ID,
|
|
"requestId": request_id,
|
|
"action": action,
|
|
"data": data,
|
|
}))
|
|
.await;
|
|
assert!(sent);
|
|
}
|
|
|
|
async fn send_response(
|
|
driver_handle: &WidgetDriverHandle,
|
|
request_id: &str,
|
|
action: &str,
|
|
request_data: impl Serialize,
|
|
response_data: impl Serialize,
|
|
) {
|
|
let sent = driver_handle
|
|
.send(json_string!({
|
|
"api": "toWidget",
|
|
"widgetId": WIDGET_ID,
|
|
"requestId": request_id,
|
|
"action": action,
|
|
"data": request_data,
|
|
"response": response_data,
|
|
}))
|
|
.await;
|
|
assert!(sent);
|
|
}
|
|
|
|
#[async_test]
|
|
async fn negotiate_capabilities_immediately() {
|
|
let (_, _, driver_handle) = run_test_driver(false).await;
|
|
|
|
let caps = json!(["org.matrix.msc2762.receive.event:m.room.message"]);
|
|
|
|
{
|
|
// Receive toWidget capabilities request
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "capabilities");
|
|
let data = &msg["data"];
|
|
let request_id = msg["requestId"].as_str().unwrap();
|
|
|
|
// Let's send a request to get supported versions in the middle
|
|
// of a capabilities negotiation to ensure that we're not "deadlocked" by
|
|
// not processing messages while waiting for a reply from a widget to the
|
|
// the toWidget request.
|
|
{
|
|
send_request(
|
|
&driver_handle,
|
|
"get-supported-api-versions",
|
|
"supported_api_versions",
|
|
json!({}),
|
|
)
|
|
.await;
|
|
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "supported_api_versions");
|
|
assert_eq!(msg["requestId"].as_str().unwrap(), "get-supported-api-versions");
|
|
assert!(msg["response"]["supported_versions"].is_array());
|
|
}
|
|
|
|
// Answer with caps we want
|
|
let response = json!({ "capabilities": caps });
|
|
send_response(&driver_handle, request_id, "capabilities", data, &response).await;
|
|
}
|
|
|
|
{
|
|
// Receive a "request" with the capabilities we were actually granted (wtf?)
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "notify_capabilities");
|
|
assert_eq!(msg["data"], json!({ "requested": caps, "approved": caps }));
|
|
let request_id = msg["requestId"].as_str().unwrap();
|
|
|
|
// ACK the request
|
|
send_response(&driver_handle, request_id, "notify_capabilities", caps, json!({})).await;
|
|
}
|
|
|
|
assert_matches!(driver_handle.recv().now_or_never(), None);
|
|
}
|
|
|
|
#[async_test]
|
|
async fn read_messages() {
|
|
let (_, mock_server, driver_handle) = run_test_driver(true).await;
|
|
|
|
{
|
|
// Tell the driver that we're ready for communication
|
|
send_request(&driver_handle, "1-content-loaded", "content_loaded", json!({})).await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "content_loaded");
|
|
assert!(msg["data"].as_object().unwrap().is_empty());
|
|
}
|
|
|
|
negotiate_capabilities(
|
|
&driver_handle,
|
|
json!(["org.matrix.msc2762.receive.event:m.room.message"]),
|
|
)
|
|
.await;
|
|
|
|
// No messages from the driver
|
|
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
|
|
|
|
{
|
|
let response_json = json!({
|
|
"chunk": [
|
|
{
|
|
"content": {
|
|
"body": "hello",
|
|
"msgtype": "m.text",
|
|
},
|
|
"event_id": "$msda7m0df9E9op3",
|
|
"origin_server_ts": 152037280,
|
|
"sender": "@example:localhost",
|
|
"type": "m.room.message",
|
|
"room_id": &*ROOM_ID,
|
|
},
|
|
{
|
|
"content": {
|
|
"body": "This room has been replaced",
|
|
"replacement_room": "!newroom:localhost",
|
|
},
|
|
"event_id": "$foun39djjod0f",
|
|
"origin_server_ts": 152039280,
|
|
"sender": "@bob:localhost",
|
|
"state_key": "",
|
|
"type": "m.room.tombstone",
|
|
"room_id": &*ROOM_ID,
|
|
},
|
|
],
|
|
"end": "t47409-4357353_219380_26003_2269",
|
|
"start": "t392-516_47314_0_7_1_1_1_11444_1"
|
|
});
|
|
Mock::given(method("GET"))
|
|
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
|
|
.and(header("authorization", "Bearer 1234"))
|
|
.and(query_param("limit", "2"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(response_json))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Ask the driver to read messages
|
|
send_request(
|
|
&driver_handle,
|
|
"2-read-messages",
|
|
"org.matrix.msc2876.read_events",
|
|
json!({
|
|
"type": "m.room.message",
|
|
"limit": 2,
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
|
|
let events = msg["response"]["events"].as_array().unwrap();
|
|
|
|
assert_eq!(events.len(), 1);
|
|
let first_event = &events[0];
|
|
assert_eq!(first_event["content"]["body"], "hello");
|
|
}
|
|
|
|
mock_server.verify().await;
|
|
}
|
|
|
|
#[async_test]
|
|
async fn read_messages_with_msgtype_capabilities() {
|
|
let (_, mock_server, driver_handle) = run_test_driver(true).await;
|
|
|
|
{
|
|
// Tell the driver that we're ready for communication
|
|
send_request(&driver_handle, "1-content-loaded", "content_loaded", json!({})).await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "content_loaded");
|
|
assert!(msg["data"].as_object().unwrap().is_empty());
|
|
}
|
|
|
|
negotiate_capabilities(
|
|
&driver_handle,
|
|
json!(["org.matrix.msc2762.receive.event:m.room.message#m.text"]),
|
|
)
|
|
.await;
|
|
|
|
// No messages from the driver
|
|
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
|
|
|
|
{
|
|
let response_json = json!({
|
|
"chunk": [
|
|
{
|
|
"content": {
|
|
"body": "custom content",
|
|
"msgtype": "m.custom.element",
|
|
},
|
|
"event_id": "$msda7m0df9E9op3",
|
|
"origin_server_ts": 152037220,
|
|
"sender": "@example:localhost",
|
|
"type": "m.room.message",
|
|
"room_id": &*ROOM_ID,
|
|
},
|
|
{
|
|
"content": {
|
|
"body": "hello",
|
|
"msgtype": "m.text",
|
|
},
|
|
"event_id": "$msda7m0df9E9op5",
|
|
"origin_server_ts": 152037280,
|
|
"sender": "@example:localhost",
|
|
"type": "m.room.message",
|
|
"room_id": &*ROOM_ID,
|
|
},
|
|
{
|
|
"content": {
|
|
},
|
|
"event_id": "$msda7m0df9E9op7",
|
|
"origin_server_ts": 152037290,
|
|
"sender": "@example:localhost",
|
|
"type": "m.reaction",
|
|
"room_id": &*ROOM_ID,
|
|
},
|
|
],
|
|
"end": "t47409-4357353_219380_26003_2269",
|
|
"start": "t392-516_47314_0_7_1_1_1_11444_1"
|
|
});
|
|
Mock::given(method("GET"))
|
|
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/messages$"))
|
|
.and(header("authorization", "Bearer 1234"))
|
|
.and(query_param("limit", "3"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(response_json))
|
|
.expect(1)
|
|
.mount(&mock_server)
|
|
.await;
|
|
|
|
// Ask the driver to read messages
|
|
send_request(
|
|
&driver_handle,
|
|
"2-read-messages",
|
|
"org.matrix.msc2876.read_events",
|
|
json!({
|
|
"type": "m.room.message",
|
|
"limit": 3,
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
|
|
let events = msg["response"]["events"].as_array().unwrap();
|
|
|
|
assert_eq!(events.len(), 1);
|
|
let first_event = &events[0];
|
|
assert_eq!(first_event["content"]["body"], "hello");
|
|
}
|
|
|
|
mock_server.verify().await;
|
|
}
|
|
|
|
#[async_test]
|
|
async fn read_room_members() {
|
|
let (_, mock_server, driver_handle) = run_test_driver(false).await;
|
|
|
|
negotiate_capabilities(
|
|
&driver_handle,
|
|
json!(["org.matrix.msc2762.receive.state_event:m.room.member"]),
|
|
)
|
|
.await;
|
|
|
|
// No messages from the driver
|
|
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
|
|
|
|
{
|
|
// The read-events request is fulfilled from the state store
|
|
drop(mock_server);
|
|
|
|
// Ask the driver to read state events
|
|
send_request(
|
|
&driver_handle,
|
|
"2-read-messages",
|
|
"org.matrix.msc2876.read_events",
|
|
json!({ "type": "m.room.member", "state_key": true }),
|
|
)
|
|
.await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "org.matrix.msc2876.read_events");
|
|
let events = msg["response"]["events"].as_array().unwrap();
|
|
|
|
// No useful data in the state store, that's fine for this test
|
|
// (we just want to know that a successful response is generated)
|
|
assert_eq!(events.len(), 0);
|
|
}
|
|
}
|
|
|
|
#[async_test]
|
|
async fn receive_live_events() {
|
|
let (client, mock_server, driver_handle) = run_test_driver(false).await;
|
|
|
|
negotiate_capabilities(
|
|
&driver_handle,
|
|
json!([
|
|
"org.matrix.msc2762.receive.event:m.room.member",
|
|
"org.matrix.msc2762.receive.event:m.room.message#m.text",
|
|
"org.matrix.msc2762.receive.state_event:m.room.name#",
|
|
"org.matrix.msc2762.receive.state_event:m.room.member#@example:localhost",
|
|
]),
|
|
)
|
|
.await;
|
|
|
|
// No messages from the driver yet
|
|
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
|
|
|
|
let mut sync_builder = SyncResponseBuilder::new();
|
|
// bump the internal batch counter, otherwise the response will be seen as
|
|
// identical to the one done in `run_test_driver`
|
|
sync_builder.build_json_sync_response();
|
|
|
|
let event_builder = EventBuilder::new();
|
|
sync_builder.add_joined_room(
|
|
JoinedRoomBuilder::new(&ROOM_ID)
|
|
// text message from alice - matches filter #2
|
|
.add_timeline_event(event_builder.make_sync_message_event(
|
|
&ALICE,
|
|
RoomMessageEventContent::text_plain("simple text message"),
|
|
))
|
|
// emote from alice - doesn't match
|
|
.add_timeline_event(event_builder.make_sync_message_event(
|
|
&ALICE,
|
|
RoomMessageEventContent::emote_plain("emote message"),
|
|
))
|
|
// pointless member event - matches filter #4
|
|
.add_timeline_event(event_builder.make_sync_state_event(
|
|
user_id!("@example:localhost"),
|
|
"@example:localhost",
|
|
RoomMemberEventContent::new(MembershipState::Join),
|
|
Some(RoomMemberEventContent::new(MembershipState::Join)),
|
|
))
|
|
// kick alice - doesn't match because the `#@example:localhost` bit
|
|
// is about the state_key, not the sender
|
|
.add_timeline_event(event_builder.make_sync_state_event(
|
|
user_id!("@example:localhost"),
|
|
ALICE.as_str(),
|
|
RoomMemberEventContent::new(MembershipState::Ban),
|
|
Some(RoomMemberEventContent::new(MembershipState::Join)),
|
|
))
|
|
// set room tpoic - doesn't match
|
|
.add_timeline_event(event_builder.make_sync_state_event(
|
|
&BOB,
|
|
"",
|
|
RoomTopicEventContent::new("new room topic".to_owned()),
|
|
None,
|
|
))
|
|
// set room name - matches filter #3
|
|
.add_timeline_event(event_builder.make_sync_state_event(
|
|
&BOB,
|
|
"",
|
|
RoomNameEventContent::new("New Room Name".to_owned()),
|
|
None,
|
|
)),
|
|
);
|
|
|
|
mock_sync(&mock_server, sync_builder.build_json_sync_response(), None).await;
|
|
let _response =
|
|
client.sync_once(SyncSettings::new().timeout(Duration::from_millis(3000))).await.unwrap();
|
|
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "send_event");
|
|
assert_eq!(msg["data"]["type"], "m.room.message");
|
|
assert_eq!(msg["data"]["room_id"], ROOM_ID.as_str());
|
|
assert_eq!(msg["data"]["content"]["msgtype"], "m.text");
|
|
assert_eq!(msg["data"]["content"]["body"], "simple text message");
|
|
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "send_event");
|
|
assert_eq!(msg["data"]["type"], "m.room.member");
|
|
assert_eq!(msg["data"]["room_id"], ROOM_ID.as_str());
|
|
assert_eq!(msg["data"]["state_key"], "@example:localhost");
|
|
assert_eq!(msg["data"]["content"]["membership"], "join");
|
|
assert_eq!(msg["data"]["unsigned"]["prev_content"]["membership"], "join");
|
|
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "send_event");
|
|
assert_eq!(msg["data"]["type"], "m.room.name");
|
|
assert_eq!(msg["data"]["sender"], BOB.as_str());
|
|
assert_eq!(msg["data"]["content"]["name"], "New Room Name");
|
|
|
|
// No more messages from the driver
|
|
assert_matches!(recv_message(&driver_handle).now_or_never(), None);
|
|
}
|
|
|
|
#[async_test]
|
|
async fn send_room_message() {
|
|
let (_, mock_server, driver_handle) = run_test_driver(false).await;
|
|
|
|
negotiate_capabilities(&driver_handle, json!(["org.matrix.msc2762.send.event:m.room.message"]))
|
|
.await;
|
|
|
|
Mock::given(method("PUT"))
|
|
.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)
|
|
.await;
|
|
|
|
send_request(
|
|
&driver_handle,
|
|
"send-room-message",
|
|
"send_event",
|
|
json!({
|
|
"type": "m.room.message",
|
|
"content": {
|
|
"msgtype": "m.text",
|
|
"body": "Message from a widget!",
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "send_event");
|
|
let event_id = msg["response"]["event_id"].as_str().unwrap();
|
|
assert_eq!(event_id, "$foobar");
|
|
|
|
// Make sure the event-sending endpoint was hit exactly once
|
|
mock_server.verify().await;
|
|
}
|
|
|
|
#[async_test]
|
|
async fn send_room_name() {
|
|
let (_, mock_server, driver_handle) = run_test_driver(false).await;
|
|
|
|
negotiate_capabilities(
|
|
&driver_handle,
|
|
json!(["org.matrix.msc2762.send.state_event:m.room.name#"]),
|
|
)
|
|
.await;
|
|
|
|
Mock::given(method("PUT"))
|
|
.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)
|
|
.await;
|
|
|
|
send_request(
|
|
&driver_handle,
|
|
"send-room-name",
|
|
"send_event",
|
|
json!({
|
|
"type": "m.room.name",
|
|
"state_key": "",
|
|
"content": {
|
|
"name": "Room Name set by Widget",
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Receive the response
|
|
let msg = recv_message(&driver_handle).await;
|
|
assert_eq!(msg["api"], "fromWidget");
|
|
assert_eq!(msg["action"], "send_event");
|
|
let event_id = msg["response"]["event_id"].as_str().unwrap();
|
|
assert_eq!(event_id, "$foobar");
|
|
|
|
// Make sure the event-sending endpoint was hit exactly once
|
|
mock_server.verify().await;
|
|
}
|
|
|
|
async fn negotiate_capabilities(driver_handle: &WidgetDriverHandle, caps: JsonValue) {
|
|
{
|
|
// Receive toWidget capabilities request
|
|
let msg = recv_message(driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "capabilities");
|
|
let data = &msg["data"];
|
|
let request_id = msg["requestId"].as_str().unwrap();
|
|
|
|
// Answer with caps we want
|
|
let response = json!({ "capabilities": caps });
|
|
send_response(driver_handle, request_id, "capabilities", data, &response).await;
|
|
}
|
|
|
|
{
|
|
// Receive notification with granted capabilities
|
|
let msg = recv_message(driver_handle).await;
|
|
assert_eq!(msg["api"], "toWidget");
|
|
assert_eq!(msg["action"], "notify_capabilities");
|
|
assert_eq!(msg["data"], json!({ "requested": caps, "approved": caps }));
|
|
let request_id = msg["requestId"].as_str().unwrap();
|
|
|
|
// ACK the notification
|
|
send_response(driver_handle, request_id, "notify_capabilities", caps, json!({})).await;
|
|
}
|
|
}
|