notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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(&note_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 }