message.rs (13733B)
1 use crate::{Error, Result}; 2 use ewebsock::{WsEvent, WsMessage}; 3 4 #[derive(Debug, Eq, PartialEq)] 5 pub struct CommandResult<'a> { 6 event_id: &'a str, 7 status: bool, 8 message: &'a str, 9 } 10 11 pub fn calculate_command_result_size(result: &CommandResult) -> usize { 12 std::mem::size_of_val(result) + result.event_id.len() + result.message.len() 13 } 14 15 #[derive(Debug, Eq, PartialEq)] 16 pub enum RelayMessage<'a> { 17 OK(CommandResult<'a>), 18 Eose(&'a str), 19 Event(&'a str, &'a str), 20 Notice(&'a str), 21 Closed(&'a str, &'a str), 22 } 23 24 #[derive(Debug)] 25 pub enum RelayEvent<'a> { 26 Opened, 27 Closed, 28 Other(&'a WsMessage), 29 Error(Error), 30 Message(RelayMessage<'a>), 31 } 32 33 impl<'a> From<&'a WsEvent> for RelayEvent<'a> { 34 fn from(event: &'a WsEvent) -> RelayEvent<'a> { 35 match event { 36 WsEvent::Opened => RelayEvent::Opened, 37 WsEvent::Closed => RelayEvent::Closed, 38 WsEvent::Message(ref ws_msg) => ws_msg.into(), 39 WsEvent::Error(s) => RelayEvent::Error(Error::Generic(s.to_owned())), 40 } 41 } 42 } 43 44 impl<'a> From<&'a WsMessage> for RelayEvent<'a> { 45 fn from(wsmsg: &'a WsMessage) -> RelayEvent<'a> { 46 match wsmsg { 47 WsMessage::Text(s) => { 48 // NIP-77 negentropy messages are handled separately via NegEvent 49 if s.starts_with("[\"NEG-") { 50 return RelayEvent::Other(wsmsg); 51 } 52 match RelayMessage::from_json(s).map(RelayEvent::Message) { 53 Ok(msg) => msg, 54 Err(err) => RelayEvent::Error(err), 55 } 56 } 57 wsmsg => RelayEvent::Other(wsmsg), 58 } 59 } 60 } 61 62 impl<'a> RelayMessage<'a> { 63 pub fn eose(subid: &'a str) -> Self { 64 RelayMessage::Eose(subid) 65 } 66 67 pub fn notice(msg: &'a str) -> Self { 68 RelayMessage::Notice(msg) 69 } 70 71 pub fn ok(event_id: &'a str, status: bool, message: &'a str) -> Self { 72 RelayMessage::OK(CommandResult { 73 event_id, 74 status, 75 message, 76 }) 77 } 78 79 pub fn event(ev: &'a str, sub_id: &'a str) -> Self { 80 RelayMessage::Event(sub_id, ev) 81 } 82 83 /// Construct a relay `CLOSED` message with its subscription id and reason. 84 pub fn closed(sub_id: &'a str, message: &'a str) -> Self { 85 RelayMessage::Closed(sub_id, message) 86 } 87 88 pub fn from_json(msg: &'a str) -> Result<RelayMessage<'a>> { 89 if msg.is_empty() { 90 return Err(Error::Empty); 91 } 92 93 // make sure we can inspect the begning of the message below ... 94 if msg.len() < 12 { 95 return Err(Error::DecodeFailed("message too short".into())); 96 } 97 98 // Notice 99 // Relay response format: ["NOTICE", <message>] 100 if msg.len() >= 12 && &msg[0..=9] == "[\"NOTICE\"," { 101 // TODO: there could be more than one space, whatever 102 let start = if msg.as_bytes().get(10).copied() == Some(b' ') { 103 12 104 } else { 105 11 106 }; 107 let end = msg.len() - 2; 108 return Ok(Self::notice(&msg[start..end])); 109 } 110 111 // Event 112 // Relay response format: ["EVENT", <subscription id>, <event JSON>] 113 if &msg[0..=7] == "[\"EVENT\"" { 114 let mut start = 9; 115 while let Some(&b' ') = msg.as_bytes().get(start) { 116 start += 1; // Move past optional spaces 117 } 118 if let Some(comma_index) = msg[start..].find(',') { 119 let subid_end = start + comma_index; 120 let subid = &msg[start..subid_end].trim().trim_matches('"'); 121 return Ok(Self::event(msg, subid)); 122 } else { 123 return Err(Error::DecodeFailed("Invalid EVENT format".into())); 124 } 125 } 126 127 // EOSE (NIP-15) 128 // Relay response format: ["EOSE", <subscription_id>] 129 if &msg[0..=7] == "[\"EOSE\"," { 130 let start = if msg.as_bytes().get(8).copied() == Some(b' ') { 131 10 // Skip space after the comma 132 } else { 133 9 // Start immediately after the comma 134 }; 135 136 // Use rfind to locate the last quote 137 if let Some(end_bracket_index) = msg.rfind(']') { 138 let end = end_bracket_index - 1; // Account for space before bracket 139 if start < end { 140 // Trim subscription id and remove extra spaces and quotes 141 let subid = &msg[start..end].trim().trim_matches('"').trim(); 142 return Ok(RelayMessage::eose(subid)); 143 } 144 } 145 return Err(Error::DecodeFailed( 146 "Invalid subscription ID or format".into(), 147 )); 148 } 149 150 // CLOSED (NIP-01) 151 // Relay response format: ["CLOSED", <subscription_id>, <message>] 152 if msg.starts_with("[\"CLOSED\"") { 153 let parts: Vec<&'a str> = 154 serde_json::from_str(msg).map_err(|err| Error::DecodeFailed(err.to_string()))?; 155 if parts.len() != 3 || parts[0] != "CLOSED" { 156 return Err(Error::DecodeFailed("Invalid CLOSED format".into())); 157 } 158 159 return Ok(Self::closed(parts[1], parts[2])); 160 } 161 162 // OK (NIP-20) 163 // Relay response format: ["OK",<event_id>, <true|false>, <message>] 164 if &msg[0..=5] == "[\"OK\"," && msg.len() >= 78 { 165 let event_id = &msg[7..71]; 166 let booly = &msg[73..77]; 167 let status: bool = if booly == "true" { 168 true 169 } else if booly == "false" { 170 false 171 } else { 172 return Err(Error::DecodeFailed("bad boolean value".into())); 173 }; 174 let message_start = msg.rfind(',').unwrap() + 1; 175 let message = &msg[message_start..msg.len() - 2].trim().trim_matches('"'); 176 return Ok(Self::ok(event_id, status, message)); 177 } 178 179 Err(Error::DecodeFailed(format!( 180 "unrecognized message type: '{msg}'" 181 ))) 182 } 183 } 184 185 #[cfg(test)] 186 mod tests { 187 use super::*; 188 189 #[test] 190 fn test_handle_various_messages() -> Result<()> { 191 let tests = vec![ 192 // Valid cases 193 ( 194 // shortest valid message 195 r#"["EOSE","x"]"#, 196 Ok(RelayMessage::eose("x")), 197 ), 198 ( 199 // also very short 200 r#"["NOTICE",""]"#, 201 Ok(RelayMessage::notice("")), 202 ), 203 ( 204 r#"["NOTICE","Invalid event format!"]"#, 205 Ok(RelayMessage::notice("Invalid event format!")), 206 ), 207 ( 208 r#"["EVENT", "random_string", {"id":"example","content":"test"}]"#, 209 Ok(RelayMessage::event( 210 r#"["EVENT", "random_string", {"id":"example","content":"test"}]"#, 211 "random_string", 212 )), 213 ), 214 ( 215 r#"["EOSE","random-subscription-id"]"#, 216 Ok(RelayMessage::eose("random-subscription-id")), 217 ), 218 ( 219 r#"["EOSE", "random-subscription-id"]"#, 220 Ok(RelayMessage::eose("random-subscription-id")), 221 ), 222 ( 223 r#"["EOSE", "random-subscription-id" ]"#, 224 Ok(RelayMessage::eose("random-subscription-id")), 225 ), 226 ( 227 r#"["CLOSED","sub1","error: shutting down idle subscription"]"#, 228 Ok(RelayMessage::closed( 229 "sub1", 230 "error: shutting down idle subscription", 231 )), 232 ), 233 ( 234 r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#, 235 Ok(RelayMessage::ok( 236 "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", 237 true, 238 "pow: difficulty 25>=24", 239 )), 240 ), 241 // Invalid cases 242 ( 243 r#"["EVENT","random_string"]"#, 244 Err(Error::DecodeFailed("Invalid EVENT format".into())), 245 ), 246 ( 247 r#"["EOSE"]"#, 248 Err(Error::DecodeFailed("message too short".into())), 249 ), 250 ( 251 r#"["NOTICE"]"#, 252 Err(Error::DecodeFailed("message too short".into())), 253 ), 254 ( 255 r#"["NOTICE": 404]"#, 256 Err(Error::DecodeFailed("unrecognized message type: '[\"NOTICE\": 404]'".into())), 257 ), 258 ( 259 r#"["OK","event_id"]"#, 260 Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"event_id\"]'".into())), 261 ), 262 ( 263 r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#, 264 Err(Error::DecodeFailed("unrecognized message type: '[\"OK\",\"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30\"]'".into())), 265 ), 266 ( 267 r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#, 268 Err(Error::DecodeFailed("bad boolean value".into())), 269 ), 270 ( 271 r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,404]"#, 272 Err(Error::DecodeFailed("bad boolean value".into())), 273 ), 274 ( 275 r#"["CLOSED","sub1"]"#, 276 Err(Error::DecodeFailed("Invalid CLOSED format".into())), 277 ), 278 ]; 279 280 for (input, expected) in tests { 281 match expected { 282 Ok(expected_msg) => { 283 let result = RelayMessage::from_json(input); 284 assert_eq!( 285 result?, expected_msg, 286 "Expected {:?} for input: {}", 287 expected_msg, input 288 ); 289 } 290 Err(expected_err) => { 291 let result = RelayMessage::from_json(input); 292 assert!( 293 matches!(result, Err(ref e) if *e.to_string() == expected_err.to_string()), 294 "Expected error {:?} for input: {}, but got: {:?}", 295 expected_err, 296 input, 297 result 298 ); 299 } 300 } 301 } 302 Ok(()) 303 } 304 305 /* 306 #[test] 307 fn test_handle_valid_event() -> Result<()> { 308 use tracing::debug; 309 310 let valid_event_msg = r#"["EVENT", "random_string", {"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe","created_at":1612809991,"kind":1,"tags":[],"content":"test","sig":"273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502"}]"#; 311 312 let id = "70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5"; 313 let pubkey = "379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe"; 314 let created_at = 1612809991; 315 let kind = 1; 316 let tags = vec![]; 317 let content = "test"; 318 let sig = "273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502"; 319 320 let handled_event = Note::new_dummy(id, pubkey, created_at, kind, tags, content, sig).expect("ev"); 321 debug!("event {:?}", handled_event); 322 323 let msg = RelayMessage::from_json(valid_event_msg).expect("valid json"); 324 debug!("msg {:?}", msg); 325 326 let note_json = serde_json::to_string(&handled_event).expect("json ev"); 327 328 assert_eq!( 329 msg, 330 RelayMessage::event(¬e_json, "random_string") 331 ); 332 333 Ok(()) 334 } 335 336 #[test] 337 fn test_handle_invalid_event() { 338 //Mising Event field 339 let invalid_event_msg = r#"["EVENT","random_string"]"#; 340 //Event JSON with incomplete content 341 let invalid_event_msg_content = r#"["EVENT","random_string",{"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe"}]"#; 342 343 assert!(matches!( 344 RelayMessage::from_json(invalid_event_msg).unwrap_err(), 345 Error::DecodeFailed 346 )); 347 348 assert!(matches!( 349 RelayMessage::from_json(invalid_event_msg_content).unwrap_err(), 350 Error::DecodeFailed 351 )); 352 } 353 */ 354 355 // TODO: fix these tests 356 /* 357 #[test] 358 fn test_handle_invalid_eose() { 359 // Missing subscription ID 360 assert!(matches!( 361 RelayMessage::from_json(r#"["EOSE"]"#).unwrap_err(), 362 Error::DecodeFailed 363 )); 364 365 // The subscription ID is not string 366 assert!(matches!( 367 RelayMessage::from_json(r#"["EOSE",404]"#).unwrap_err(), 368 Error::DecodeFailed 369 )); 370 } 371 372 #[test] 373 fn test_handle_valid_ok() -> Result<()> { 374 let valid_ok_msg = r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#; 375 let handled_valid_ok_msg = RelayMessage::ok( 376 "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", 377 true, 378 "pow: difficulty 25>=24".into(), 379 ); 380 381 assert_eq!(RelayMessage::from_json(valid_ok_msg)?, handled_valid_ok_msg); 382 383 Ok(()) 384 } 385 */ 386 }