lib.rs (5549B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(target_arch = "wasm32")] 4 use base64::Engine; 5 #[cfg(target_arch = "wasm32")] 6 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 7 use radroots_events::RadrootsNostrEvent; 8 use radroots_replica_sync::RadrootsReplicaSyncRequest; 9 #[cfg(target_arch = "wasm32")] 10 use radroots_replica_sync::{ 11 RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome, 12 radroots_replica_ingest_event_with_factory, radroots_replica_sync_all, 13 }; 14 #[cfg(target_arch = "wasm32")] 15 use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor; 16 use serde::Deserialize; 17 #[cfg(target_arch = "wasm32")] 18 use uuid::Uuid; 19 #[cfg(target_arch = "wasm32")] 20 use wasm_bindgen::prelude::*; 21 22 #[cfg(target_arch = "wasm32")] 23 fn err_js<E: ToString>(err: E) -> JsValue { 24 JsValue::from_str(&err.to_string()) 25 } 26 27 #[cfg(target_arch = "wasm32")] 28 struct WasmIdFactory; 29 30 #[cfg(target_arch = "wasm32")] 31 impl RadrootsReplicaIdFactory for WasmIdFactory { 32 fn new_d_tag(&self) -> String { 33 let uuid = Uuid::now_v7(); 34 URL_SAFE_NO_PAD.encode(uuid.as_bytes()) 35 } 36 } 37 38 #[derive(Deserialize)] 39 struct NostrEventEnvelope { 40 id: String, 41 #[serde(default)] 42 author: Option<String>, 43 #[serde(default)] 44 pubkey: Option<String>, 45 created_at: u32, 46 kind: u32, 47 tags: Vec<Vec<String>>, 48 content: String, 49 sig: String, 50 } 51 52 pub fn parse_request_model(request_json: &str) -> Result<RadrootsReplicaSyncRequest, String> { 53 serde_json::from_str(request_json).map_err(|error| error.to_string()) 54 } 55 56 pub fn parse_event_model(event_json: &str) -> Result<RadrootsNostrEvent, String> { 57 let envelope: NostrEventEnvelope = 58 serde_json::from_str(event_json).map_err(|error| error.to_string())?; 59 let author = match (envelope.author, envelope.pubkey) { 60 (Some(author), Some(pubkey)) if author != pubkey => { 61 return Err("author/pubkey mismatch".to_owned()); 62 } 63 (Some(author), _) => author, 64 (None, Some(pubkey)) => pubkey, 65 (None, None) => return Err("missing author/pubkey".to_owned()), 66 }; 67 Ok(RadrootsNostrEvent { 68 id: envelope.id, 69 author, 70 created_at: envelope.created_at, 71 kind: envelope.kind, 72 tags: envelope.tags, 73 content: envelope.content, 74 sig: envelope.sig, 75 }) 76 } 77 78 #[cfg(target_arch = "wasm32")] 79 #[wasm_bindgen(js_name = replica_sync_sync_all)] 80 pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { 81 let request = parse_request_model(request_json).map_err(err_js)?; 82 let exec = WasmSqlExecutor::new(); 83 let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?; 84 serde_wasm_bindgen::to_value(&bundle).map_err(err_js) 85 } 86 87 #[cfg(target_arch = "wasm32")] 88 #[wasm_bindgen(js_name = replica_sync_ingest_event)] 89 pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { 90 let event = parse_event_model(event_json).map_err(err_js)?; 91 let exec = WasmSqlExecutor::new(); 92 let factory = WasmIdFactory; 93 let outcome = 94 radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?; 95 let value = match outcome { 96 RadrootsReplicaIngestOutcome::Applied => "applied", 97 RadrootsReplicaIngestOutcome::Skipped => "skipped", 98 }; 99 Ok(JsValue::from_str(value)) 100 } 101 102 #[cfg(test)] 103 mod tests { 104 use super::{parse_event_model, parse_request_model}; 105 106 fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String { 107 let mut fields = vec![ 108 r#""id":"event-id""#.to_owned(), 109 r#""created_at":123"#.to_owned(), 110 r#""kind":30023"#.to_owned(), 111 r#""tags":[["d","one"]]"#.to_owned(), 112 r#""content":"content""#.to_owned(), 113 r#""sig":"sig""#.to_owned(), 114 ]; 115 if let Some(author) = author { 116 fields.push(format!(r#""author":"{author}""#)); 117 } 118 if let Some(pubkey) = pubkey { 119 fields.push(format!(r#""pubkey":"{pubkey}""#)); 120 } 121 format!("{{{}}}", fields.join(",")) 122 } 123 124 #[test] 125 fn parse_event_accepts_matching_author_and_pubkey() { 126 let event = parse_event_model(&event_json(Some("author"), Some("author"))).expect("event"); 127 assert_eq!(event.author, "author"); 128 assert_eq!(event.tags, vec![vec!["d".to_owned(), "one".to_owned()]]); 129 } 130 131 #[test] 132 fn parse_event_accepts_author_without_pubkey() { 133 let event = parse_event_model(&event_json(Some("author"), None)).expect("event"); 134 assert_eq!(event.author, "author"); 135 } 136 137 #[test] 138 fn parse_event_accepts_pubkey_without_author() { 139 let event = parse_event_model(&event_json(None, Some("pubkey"))).expect("event"); 140 assert_eq!(event.author, "pubkey"); 141 } 142 143 #[test] 144 fn parse_event_rejects_author_pubkey_mismatch() { 145 let error = 146 parse_event_model(&event_json(Some("author"), Some("pubkey"))).expect_err("error"); 147 assert_eq!(error, "author/pubkey mismatch"); 148 } 149 150 #[test] 151 fn parse_event_rejects_missing_author_and_pubkey() { 152 let error = parse_event_model(&event_json(None, None)).expect_err("error"); 153 assert_eq!(error, "missing author/pubkey"); 154 } 155 156 #[test] 157 fn parse_event_rejects_malformed_json() { 158 let error = parse_event_model("{").expect_err("error"); 159 assert!(error.contains("EOF")); 160 } 161 162 #[test] 163 fn parse_request_rejects_malformed_json() { 164 let error = parse_request_model("{").expect_err("error"); 165 assert!(error.contains("EOF")); 166 } 167 }