pub mod audit;
pub mod authz;
pub mod error;
pub mod lifecycle;
#[cfg(feature = "mls")]
pub mod mls;
#[cfg(feature = "mls")]
pub mod retention;
#[cfg(feature = "mls")]
pub mod sealed;
pub mod storage;
pub mod wire;
use serde::{Deserialize, Serialize};
pub const ROOMS_KEYSPACE: &str = "rooms";
pub const ROOM_RECORDS_KEYSPACE: &str = "room_records";
pub const ROOM_EPOCH_LINKS_KEYSPACE: &str = "room_epoch_links";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RetentionPolicy {
#[default]
Chained,
FromJoin,
}
impl RetentionPolicy {
pub fn links_epochs(&self) -> bool {
matches!(self, RetentionPolicy::Chained)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Visibility {
Open,
Attributed,
Private,
}
impl Visibility {
pub fn stores_cleartext(&self) -> bool {
matches!(self, Visibility::Open)
}
pub fn discloses_actor(&self) -> bool {
matches!(self, Visibility::Open | Visibility::Attributed)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Room {
pub room_id: String,
pub owner_did: String,
pub visibility: Visibility,
#[serde(default)]
pub retention_policy: RetentionPolicy,
pub epoch: u32,
pub next_version: u64,
pub retention_days: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub epoch_expires_at: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mirror_of: Option<String>,
pub created_at: u64,
pub updated_at: u64,
}
impl Room {
pub fn is_mirror(&self) -> bool {
self.mirror_of.is_some()
}
pub fn watermark(&self) -> u64 {
self.next_version.saturating_sub(1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RecordStatus {
Active,
Deprecated,
Retracted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Record {
pub key: String,
pub version: u64,
pub epoch: Option<u32>,
pub status: RecordStatus,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub pinned: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub sealed: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cleartext: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
pub updated_at: u64,
}
fn rfc3339(unix_seconds: u64) -> String {
chrono::DateTime::from_timestamp(unix_seconds as i64, 0)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).expect("epoch is in range"))
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
impl Record {
pub fn metadata(&self) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert("key".into(), serde_json::json!(self.key));
map.insert("version".into(), serde_json::json!(self.version));
map.insert("status".into(), serde_json::json!(self.status));
map.insert(
"updatedAt".into(),
serde_json::json!(rfc3339(self.updated_at)),
);
if let Some(epoch) = self.epoch {
map.insert("epoch".into(), serde_json::json!(epoch));
}
if let Some(author) = &self.author {
map.insert("author".into(), serde_json::json!(author));
}
if let Some(cleartext) = &self.cleartext {
for field in ["title", "description"] {
if let Some(v) = cleartext.get(field).filter(|v| v.is_string()) {
map.insert(field.into(), v.clone());
}
}
}
serde_json::Value::Object(map)
}
}
#[cfg(test)]
mod retention_policy_tests {
use super::*;
#[test]
fn a_room_stored_before_the_chain_existed_chains_from_here() {
let json = r#"{
"roomId": "did:webvh:example.com:rooms:legacy",
"ownerDid": "did:key:zOwner",
"visibility": "attributed",
"epoch": 4,
"nextVersion": 9,
"retentionDays": 90,
"createdAt": 0,
"updatedAt": 0
}"#;
let room: Room = serde_json::from_str(json).expect("a pre-chain room deserialises");
assert_eq!(room.retention_policy, RetentionPolicy::Chained);
assert!(
room.retention_policy.links_epochs(),
"a legacy room must be able to chain from here, whatever it lost before"
);
}
#[test]
fn only_a_chained_room_accepts_rungs() {
assert!(RetentionPolicy::Chained.links_epochs());
assert!(!RetentionPolicy::FromJoin.links_epochs());
}
}