use serde_json::{Value, json};
use super::VtaClient;
use crate::error::VtaError;
use crate::trust_tasks;
use vti_specs::rooms::keys::{act::v0_12 as act, changed::v0_1 as changed, watch::v0_1 as watch};
pub const ROOM_WATCH_MAX_ROOMS: usize = 64;
const ROOM_WATCH_TIMEOUT: u64 = 120;
const ROOM_ACT_TIMEOUT: u64 = 30;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoomWatch {
pub room_id: String,
pub since_version: Option<u64>,
}
impl VtaClient {
pub async fn room_watch(
&self,
member: &str,
host: &str,
rooms: &[RoomWatch],
lease_seconds: Option<u64>,
) -> Result<watch::Response, VtaError> {
let rooms: Vec<Value> = rooms
.iter()
.map(|r| {
let mut room = json!({ "roomId": r.room_id });
if let Some(v) = r.since_version {
room["sinceVersion"] = json!(v);
}
room
})
.collect();
let mut payload = json!({ "member": member, "host": host, "rooms": rooms });
if let Some(l) = lease_seconds {
payload["leaseSeconds"] = json!(l);
}
self.send_room_watch(payload).await
}
pub async fn room_watch_cancel(
&self,
member: &str,
host: &str,
rooms: &[&str],
) -> Result<watch::Response, VtaError> {
let rooms: Vec<Value> = rooms.iter().map(|r| json!({ "roomId": r })).collect();
self.send_room_watch(
json!({ "member": member, "host": host, "rooms": rooms, "cancel": true }),
)
.await
}
async fn send_room_watch(&self, payload: Value) -> Result<watch::Response, VtaError> {
let rooms = payload["rooms"].as_array().map_or(0, Vec::len);
if rooms == 0 || rooms > ROOM_WATCH_MAX_ROOMS {
return Err(VtaError::Validation(format!(
"a watch names 1 to {ROOM_WATCH_MAX_ROOMS} rooms at one host, not {rooms}"
)));
}
let value = self
.dispatch_trust_task(
trust_tasks::TASK_ROOMS_KEYS_WATCH_0_1,
payload,
ROOM_WATCH_TIMEOUT,
)
.await?;
serde_json::from_value(value).map_err(|e| {
VtaError::Protocol(format!(
"rooms/keys/watch response does not match its schema: {e}"
))
})
}
pub async fn room_act_v0_12(
&self,
document: &act::UnsignedRoomRequest,
) -> Result<act::Response, VtaError> {
let value = self
.dispatch_trust_task(
trust_tasks::TASK_ROOMS_KEYS_ACT_0_12,
json!({ "document": document }),
ROOM_ACT_TIMEOUT,
)
.await?;
serde_json::from_value(value).map_err(|e| {
VtaError::Protocol(format!(
"rooms/keys/act response does not match its schema: {e}"
))
})
}
}
pub fn unsigned_room_request_v0_12(
type_uri: &str,
host: &str,
issuer: Option<&str>,
payload: Value,
) -> Result<act::UnsignedRoomRequest, VtaError> {
let base = super::unsigned_room_request(type_uri, host, issuer, payload)?;
serde_json::from_value(serde_json::to_value(base)?)
.map_err(|e| VtaError::Validation(format!("not a room request this can complete: {e}")))
}
pub fn room_changed_signal(document: &Value) -> Option<changed::Payload> {
let type_uri = document.get("type")?.as_str()?;
if type_uri.split('#').next() != Some(trust_tasks::TASK_ROOMS_KEYS_CHANGED_0_1) {
return None;
}
serde_json::from_value(document.get("payload")?.clone()).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_signal_is_read_only_from_its_own_type() {
let doc = json!({
"type": trust_tasks::TASK_ROOMS_KEYS_CHANGED_0_1,
"payload": {
"roomId": "did:example:room",
"member": "did:example:member",
"host": "did:example:host",
"latestVersion": 415
}
});
let s = room_changed_signal(&doc).expect("a signal");
assert_eq!(s.latest_version.get(), 415);
let mut other = doc.clone();
other["type"] = json!(trust_tasks::TASK_ROOMS_CHANGED_0_1);
assert!(room_changed_signal(&other).is_none());
let mut extra = doc;
extra["payload"]["address"] = json!("did:example:address");
assert!(
room_changed_signal(&extra).is_none(),
"the schema is closed: an address never rides a pass-on"
);
}
#[test]
fn an_act_0_12_document_carries_a_watch() {
let d = unsigned_room_request_v0_12(
"https://trusttasks.org/spec/rooms/watch/0.1",
"did:example:host",
None,
json!({ "roomId": "did:example:room", "address": "did:example:address" }),
)
.unwrap();
let v = serde_json::to_value(d).unwrap();
assert_eq!(v["payload"]["address"], "did:example:address");
}
}