notedeck

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

message.rs (12359B)


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