// Copyright (c) 2022-2024 Niko Bonnieure, Par le Peuple, NextGraph.org developers // All rights reserved. // Licensed under the Apache License, Version 2.0 // // or the MIT license , // at your option. All files in the project carrying such // notice may not be copied, modified, or distributed except // according to those terms. //! App Protocol (between LocalBroker and Verifier) use lazy_static::lazy_static; use regex::Regex; use serde::{Deserialize, Serialize}; use ng_repo::errors::NgError; #[allow(unused_imports)] use ng_repo::log::*; use ng_repo::repo::CommitInfo; use ng_repo::types::*; use ng_repo::utils::decode_overlayid; use ng_repo::utils::{decode_digest, decode_key, decode_sym_key}; use crate::types::*; lazy_static! { #[doc(hidden)] static ref RE_FILE_READ_CAP: Regex = Regex::new(r"^did:ng:j:([A-Za-z0-9-_]*):k:([A-Za-z0-9-_]*)$").unwrap(); #[doc(hidden)] static ref RE_REPO: Regex = Regex::new(r"^did:ng:o:([A-Za-z0-9-_]*):v:([A-Za-z0-9-_]*)$").unwrap(); #[doc(hidden)] static ref RE_BRANCH: Regex = Regex::new(r"^did:ng:o:([A-Za-z0-9-_]*):v:([A-Za-z0-9-_]*):b:([A-Za-z0-9-_]*)$").unwrap(); #[doc(hidden)] static ref RE_NAMED_BRANCH_OR_COMMIT: Regex = Regex::new(r"^did:ng:o:([A-Za-z0-9-_]*):v:([A-Za-z0-9-_]*):a:([A-Za-z0-9-_%]*)$").unwrap(); //TODO: allow international chars. disallow digit as first char } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppFetchContentV0 { Get, // does not subscribe. more to be detailed Subscribe, // more to be detailed Update, //Invoke, ReadQuery, // more to be detailed WriteQuery, // more to be detailed RdfDump, History, } impl AppFetchContentV0 { pub fn get_or_subscribe(subscribe: bool) -> Self { if !subscribe { AppFetchContentV0::Get } else { AppFetchContentV0::Subscribe } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum NgAccessV0 { ReadCap(ReadCap), Token(Digest), #[serde(with = "serde_bytes")] ExtRequest(Vec), Key(BlockKey), Inbox(Digest), } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum TargetBranchV0 { Chat, Stream, Comments, BackLinks, Context, Ontology, BranchId(BranchId), Named(String), // branch or commit Commits(Vec), // only possible if access to their branch is given. must belong to the same branch. } impl TargetBranchV0 { pub fn is_valid_for_sparql_update(&self) -> bool { match self { Self::Commits(_) => false, _ => true, } } pub fn branch_id(&self) -> &BranchId { match self { Self::BranchId(id) => id, _ => panic!("not a TargetBranchV0::BranchId"), } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum NuriTargetV0 { UserSite, // targets the whole data set of the user PublicStore, ProtectedStore, PrivateStore, AllDialogs, Dialog(String), // shortname of a Dialog AllGroups, Group(String), // shortname of a Group Repo(RepoId), None, } impl NuriTargetV0 { pub fn is_valid_for_sparql_update(&self) -> bool { match self { Self::UserSite | Self::AllDialogs | Self::AllGroups => false, _ => true, } } pub fn is_repo_id(&self) -> bool { match self { Self::Repo(_) => true, _ => false, } } pub fn repo_id(&self) -> &RepoId { match self { Self::Repo(id) => id, _ => panic!("not a NuriTargetV0::Repo"), } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct CommitInfoJs { pub past: Vec, pub key: String, pub signature: Option, pub author: String, pub final_consistency: bool, pub commit_type: CommitType, pub branch: String, pub x: u32, pub y: u32, } impl From<&CommitInfo> for CommitInfoJs { fn from(info: &CommitInfo) -> Self { CommitInfoJs { past: info.past.iter().map(|objid| objid.to_string()).collect(), key: info.key.to_string(), signature: info.signature.as_ref().map(|s| NuriV0::object_ref(&s)), author: info.author.clone(), final_consistency: info.final_consistency, commit_type: info.commit_type.clone(), branch: info.branch.unwrap().to_string(), x: info.x, y: info.y, } } } const DID_PREFIX: &str = "did:ng"; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NuriV0 { pub identity: Option, // None for personal identity pub target: NuriTargetV0, pub entire_store: bool, // If it is a store, will include all the docs belonging to the store pub object: Option, // used only for FileGet. // cannot be used for queries. only to download an object (file,commit..) pub branch: Option, // if None, the main branch is chosen pub overlay: Option, pub access: Vec, pub topic: Option, pub locator: Vec, } impl NuriV0 { pub fn commit_graph_name(commit_id: &ObjectId, overlay_id: &OverlayId) -> String { format!("{DID_PREFIX}:c:{commit_id}:v:{overlay_id}") } pub fn commit_graph_name_from_base64(commit_base64: &String, overlay_id: &OverlayId) -> String { format!("{DID_PREFIX}:c:{commit_base64}:v:{overlay_id}") } pub fn repo_graph_name(repo_id: &RepoId, overlay_id: &OverlayId) -> String { format!("{DID_PREFIX}:o:{repo_id}:v:{overlay_id}") } pub fn overlay_id(overlay_id: &OverlayId) -> String { format!("{DID_PREFIX}:v:{overlay_id}") } pub fn topic_id(topic_id: &TopicId) -> String { format!("{DID_PREFIX}:h:{topic_id}") } pub fn branch_id(branch_id: &BranchId) -> String { format!("{DID_PREFIX}:b:{branch_id}") } pub fn branch_id_from_base64(branch_base64: &String) -> String { format!("{DID_PREFIX}:b:{branch_base64}") } pub fn object_ref(obj_ref: &ObjectRef) -> String { format!("{DID_PREFIX}{}", obj_ref.nuri()) } pub fn token(token: &Digest) -> String { format!("{DID_PREFIX}:n:{token}") } pub fn is_branch_identifier(&self) -> bool { self.locator.is_empty() && self.topic.is_none() && self.access.is_empty() && self.overlay.as_ref().map_or(false, |o| o.is_outer()) && self .branch .as_ref() .map_or(true, |b| b.is_valid_for_sparql_update()) && self.object.is_none() && !self.entire_store && self.target.is_repo_id() } pub fn is_valid_for_sparql_update(&self) -> bool { self.object.is_none() && self.entire_store == false && self.target.is_valid_for_sparql_update() && self .branch .as_ref() .map_or(true, |b| b.is_valid_for_sparql_update()) } pub fn new_repo_target_from_string(repo_id_string: String) -> Result { let repo_id: RepoId = repo_id_string.as_str().try_into()?; Ok(Self { identity: None, target: NuriTargetV0::Repo(repo_id), entire_store: false, object: None, branch: None, overlay: None, access: vec![], topic: None, locator: vec![], }) } pub fn new_private_store_target() -> Self { Self { identity: None, target: NuriTargetV0::PrivateStore, entire_store: false, object: None, branch: None, overlay: None, access: vec![], topic: None, locator: vec![], } } pub fn new_entire_user_site() -> Self { Self { identity: None, target: NuriTargetV0::UserSite, entire_store: false, object: None, branch: None, overlay: None, access: vec![], topic: None, locator: vec![], } } pub fn new_from(from: &String) -> Result { let c = RE_FILE_READ_CAP.captures(from); if c.is_some() && c.as_ref().unwrap().get(1).is_some() && c.as_ref().unwrap().get(2).is_some() { let cap = c.unwrap(); let j = cap.get(1).unwrap().as_str(); let k = cap.get(2).unwrap().as_str(); let id = decode_digest(j)?; let key = decode_sym_key(k)?; Ok(Self { identity: None, target: NuriTargetV0::PrivateStore, entire_store: false, object: Some(id), branch: None, overlay: None, access: vec![NgAccessV0::Key(key)], topic: None, locator: vec![], }) } else { let c = RE_REPO.captures(from); if c.is_some() && c.as_ref().unwrap().get(1).is_some() && c.as_ref().unwrap().get(2).is_some() { let cap = c.unwrap(); let o = cap.get(1).unwrap().as_str(); let v = cap.get(2).unwrap().as_str(); let repo_id = decode_key(o)?; let overlay_id = decode_overlayid(v)?; Ok(Self { identity: None, target: NuriTargetV0::Repo(repo_id), entire_store: false, object: None, branch: None, overlay: Some(overlay_id.into()), access: vec![], topic: None, locator: vec![], }) } else { let c = RE_BRANCH.captures(from); if c.is_some() && c.as_ref().unwrap().get(1).is_some() && c.as_ref().unwrap().get(2).is_some() && c.as_ref().unwrap().get(3).is_some() { let cap = c.unwrap(); let o = cap.get(1).unwrap().as_str(); let v = cap.get(2).unwrap().as_str(); let b = cap.get(3).unwrap().as_str(); let repo_id = decode_key(o)?; let overlay_id = decode_overlayid(v)?; let branch_id = decode_key(b)?; Ok(Self { identity: None, target: NuriTargetV0::Repo(repo_id), entire_store: false, object: None, branch: Some(TargetBranchV0::BranchId(branch_id)), overlay: Some(overlay_id.into()), access: vec![], topic: None, locator: vec![], }) } else { Err(NgError::InvalidNuri) } } } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppRequestCommandV0 { Fetch(AppFetchContentV0), Pin, UnPin, Delete, Create, FileGet, // needs the Nuri of branch/doc/store AND ObjectId FilePut, // needs the Nuri of branch/doc/store } impl AppRequestCommandV0 { pub fn is_stream(&self) -> bool { match self { Self::Fetch(AppFetchContentV0::Subscribe) | Self::FileGet => true, Self::FilePut | Self::Create | Self::Delete | Self::UnPin | Self::Pin | Self::Fetch(_) => false, } } pub fn new_read_query() -> Self { AppRequestCommandV0::Fetch(AppFetchContentV0::ReadQuery) } pub fn new_write_query() -> Self { AppRequestCommandV0::Fetch(AppFetchContentV0::WriteQuery) } pub fn new_rdf_dump() -> Self { AppRequestCommandV0::Fetch(AppFetchContentV0::RdfDump) } pub fn new_history() -> Self { AppRequestCommandV0::Fetch(AppFetchContentV0::History) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppRequestV0 { pub command: AppRequestCommandV0, pub nuri: NuriV0, pub payload: Option, pub session_id: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppRequest { V0(AppRequestV0), } impl AppRequest { pub fn set_session_id(&mut self, session_id: u64) { match self { Self::V0(v0) => v0.session_id = session_id, } } pub fn session_id(&self) -> u64 { match self { Self::V0(v0) => v0.session_id, } } pub fn command(&self) -> &AppRequestCommandV0 { match self { Self::V0(v0) => &v0.command, } } pub fn new( command: AppRequestCommandV0, nuri: NuriV0, payload: Option, ) -> Self { AppRequest::V0(AppRequestV0 { command, nuri, payload, session_id: 0, }) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppSessionStopV0 { pub session_id: u64, pub force_close: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppSessionStop { V0(AppSessionStopV0), } impl AppSessionStop { pub fn session_id(&self) -> u64 { match self { Self::V0(v0) => v0.session_id, } } pub fn is_force_close(&self) -> bool { match self { Self::V0(v0) => v0.force_close, } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppSessionStartV0 { pub session_id: u64, pub credentials: Option, pub user_id: UserId, pub detach: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppSessionStart { V0(AppSessionStartV0), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppSessionStartResponseV0 { pub private_store: RepoId, pub protected_store: RepoId, pub public_store: RepoId, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppSessionStartResponse { V0(AppSessionStartResponseV0), } impl AppSessionStart { pub fn session_id(&self) -> u64 { match self { Self::V0(v0) => v0.session_id, } } pub fn credentials(&self) -> &Option { match self { Self::V0(v0) => &v0.credentials, } } pub fn user_id(&self) -> &UserId { match self { Self::V0(v0) => &v0.user_id, } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DocQuery { V0(String), // Sparql } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GraphUpdate { // serialization of Vec #[serde(with = "serde_bytes")] pub inserts: Vec, // serialization of Vec #[serde(with = "serde_bytes")] pub removes: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DiscreteUpdate { /// A yrs::Update #[serde(with = "serde_bytes")] YMap(Vec), #[serde(with = "serde_bytes")] YXml(Vec), #[serde(with = "serde_bytes")] YText(Vec), /// An automerge::Patch #[serde(with = "serde_bytes")] Automerge(Vec), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocUpdate { heads: Vec, graph: Option, discrete: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocAddFile { pub filename: Option, pub object: ObjectRef, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocCreate { store: StoreRepo, content_type: BranchContentType, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DocDelete { /// Nuri of doc to delete nuri: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppRequestPayloadV0 { Create(DocCreate), Query(DocQuery), Update(DocUpdate), AddFile(DocAddFile), //RemoveFile Delete(DocDelete), //Invoke(InvokeArguments), SmallFilePut(SmallFile), RandomAccessFilePut(String), // content_type RandomAccessFilePutChunk((u32, serde_bytes::ByteBuf)), // end the upload with an empty vec } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppRequestPayload { V0(AppRequestPayloadV0), } impl AppRequestPayload { pub fn new_sparql_query(query: String) -> Self { AppRequestPayload::V0(AppRequestPayloadV0::Query(DocQuery::V0(query))) } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DiscretePatch { /// A yrs::Update #[serde(with = "serde_bytes")] YMap(Vec), #[serde(with = "serde_bytes")] YXml(Vec), #[serde(with = "serde_bytes")] YText(Vec), /// An automerge::Patch #[serde(with = "serde_bytes")] Automerge(Vec), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GraphPatch { // serialization of Vec #[serde(with = "serde_bytes")] pub inserts: Vec, // serialization of Vec #[serde(with = "serde_bytes")] pub removes: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DiscreteState { /// A yrs::StateVector #[serde(with = "serde_bytes")] YMap(Vec), #[serde(with = "serde_bytes")] YXml(Vec), #[serde(with = "serde_bytes")] YText(Vec), // the output of Automerge::save() #[serde(with = "serde_bytes")] Automerge(Vec), } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct GraphState { // serialization of Vec #[serde(with = "serde_bytes")] pub triples: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppState { pub heads: Vec, pub graph: Option, // there is always a graph present in the branch. but it might not have been asked in the request pub discrete: Option, pub files: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppHistory { pub history: Vec<(ObjectId, CommitInfo)>, pub swimlane_state: Vec>, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppHistoryJs { pub history: Vec<(String, CommitInfoJs)>, pub swimlane_state: Vec>, } impl AppHistory { pub fn to_js(&self) -> AppHistoryJs { AppHistoryJs { history: Vec::from_iter( self.history .iter() .map(|(id, info)| (id.to_string(), info.into())), ), swimlane_state: Vec::from_iter( self.swimlane_state .iter() .map(|lane| lane.map_or(None, |b| Some(b.to_string()))), ), } } } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum OtherPatch { FileAdd(FileName), FileRemove(ObjectId), AsyncSignature((ObjectRef, Vec)), Snapshot(ObjectRef), Compact(ObjectRef), Other, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AppPatch { pub commit_id: ObjectId, pub commit_info: CommitInfo, // or graph, or discrete, or both, or other. pub graph: Option, pub discrete: Option, pub other: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileName { pub name: Option, pub reference: ObjectRef, pub nuri: String, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileMetaV0 { pub content_type: String, pub size: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppResponseV0 { SessionStart(AppSessionStartResponse), State(AppState), Patch(AppPatch), History(AppHistory), Text(String), //File(FileName), FileUploading(u32), FileUploaded(ObjectRef), #[serde(with = "serde_bytes")] FileBinary(Vec), FileMeta(FileMetaV0), #[serde(with = "serde_bytes")] QueryResult(Vec), // a serialized [SPARQL Query Results JSON Format](https://www.w3.org/TR/sparql11-results-json/) #[serde(with = "serde_bytes")] Graph(Vec), // a serde serialization of a list of triples. can be transformed on the client side to RDF-JS data model, or JSON-LD, or else (Turtle,...) http://rdf.js.org/data-model-spec/ Ok, True, False, Error(String), EndOfStream, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum AppResponse { V0(AppResponseV0), } impl AppResponse { pub fn error(err: String) -> Self { AppResponse::V0(AppResponseV0::Error(err)) } pub fn ok() -> Self { AppResponse::V0(AppResponseV0::Ok) } }