lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

uri.rs (7589B)


      1 use crate::error::RadrootsNostrConnectError;
      2 use crate::permission::RadrootsNostrConnectPermissions;
      3 use nostr::{PublicKey, RelayUrl};
      4 use serde::{Deserialize, Serialize};
      5 use std::fmt;
      6 use std::str::FromStr;
      7 use url::Url;
      8 
      9 pub const RADROOTS_NOSTR_CONNECT_URI_SCHEME: &str = "nostrconnect";
     10 pub const RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME: &str = "bunker";
     11 
     12 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     13 pub struct RadrootsNostrConnectBunkerUri {
     14     pub remote_signer_public_key: PublicKey,
     15     pub relays: Vec<RelayUrl>,
     16     #[serde(skip_serializing_if = "Option::is_none")]
     17     pub secret: Option<String>,
     18 }
     19 
     20 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
     21 pub struct RadrootsNostrConnectClientMetadata {
     22     #[serde(
     23         default,
     24         skip_serializing_if = "RadrootsNostrConnectPermissions::is_empty"
     25     )]
     26     pub requested_permissions: RadrootsNostrConnectPermissions,
     27     #[serde(skip_serializing_if = "Option::is_none")]
     28     pub name: Option<String>,
     29     #[serde(skip_serializing_if = "Option::is_none")]
     30     pub url: Option<String>,
     31     #[serde(skip_serializing_if = "Option::is_none")]
     32     pub image: Option<String>,
     33 }
     34 
     35 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     36 pub struct RadrootsNostrConnectClientUri {
     37     pub client_public_key: PublicKey,
     38     pub relays: Vec<RelayUrl>,
     39     pub secret: String,
     40     #[serde(default)]
     41     pub metadata: RadrootsNostrConnectClientMetadata,
     42 }
     43 
     44 #[derive(Debug, Clone, PartialEq, Eq)]
     45 pub enum RadrootsNostrConnectUri {
     46     Bunker(RadrootsNostrConnectBunkerUri),
     47     Client(RadrootsNostrConnectClientUri),
     48 }
     49 
     50 impl RadrootsNostrConnectUri {
     51     pub fn parse(value: &str) -> Result<Self, RadrootsNostrConnectError> {
     52         let url = Url::parse(value).map_err(|error| RadrootsNostrConnectError::InvalidUrl {
     53             value: value.to_owned(),
     54             reason: error.to_string(),
     55         })?;
     56         let host = url
     57             .host_str()
     58             .ok_or(RadrootsNostrConnectError::MissingPublicKey)?;
     59 
     60         match url.scheme() {
     61             RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME => {
     62                 let remote_signer_public_key = parse_public_key(host)?;
     63                 let mut relays = Vec::new();
     64                 let mut secret = None;
     65 
     66                 for (key, value) in url.query_pairs() {
     67                     match key.as_ref() {
     68                         "relay" => relays.push(parse_relay_url(value.as_ref())?),
     69                         "secret" => secret = Some(value.into_owned()),
     70                         _ => {}
     71                     }
     72                 }
     73 
     74                 if relays.is_empty() {
     75                     return Err(RadrootsNostrConnectError::MissingRelay);
     76                 }
     77 
     78                 Ok(Self::Bunker(RadrootsNostrConnectBunkerUri {
     79                     remote_signer_public_key,
     80                     relays,
     81                     secret,
     82                 }))
     83             }
     84             RADROOTS_NOSTR_CONNECT_URI_SCHEME => {
     85                 let client_public_key = parse_public_key(host)?;
     86                 let mut relays = Vec::new();
     87                 let mut secret = None;
     88                 let mut metadata = RadrootsNostrConnectClientMetadata::default();
     89 
     90                 for (key, value) in url.query_pairs() {
     91                     match key.as_ref() {
     92                         "relay" => relays.push(parse_relay_url(value.as_ref())?),
     93                         "secret" => secret = Some(value.into_owned()),
     94                         "perms" => {
     95                             metadata.requested_permissions =
     96                                 RadrootsNostrConnectPermissions::from_str(value.as_ref())?;
     97                         }
     98                         "name" => metadata.name = Some(value.into_owned()),
     99                         "url" => metadata.url = Some(validate_url(value.as_ref())?),
    100                         "image" => metadata.image = Some(validate_url(value.as_ref())?),
    101                         _ => {}
    102                     }
    103                 }
    104 
    105                 if relays.is_empty() {
    106                     return Err(RadrootsNostrConnectError::MissingRelay);
    107                 }
    108 
    109                 let secret = secret.ok_or(RadrootsNostrConnectError::MissingSecret)?;
    110 
    111                 Ok(Self::Client(RadrootsNostrConnectClientUri {
    112                     client_public_key,
    113                     relays,
    114                     secret,
    115                     metadata,
    116                 }))
    117             }
    118             scheme => Err(RadrootsNostrConnectError::InvalidUriScheme(
    119                 scheme.to_owned(),
    120             )),
    121         }
    122     }
    123 }
    124 
    125 impl FromStr for RadrootsNostrConnectUri {
    126     type Err = RadrootsNostrConnectError;
    127 
    128     fn from_str(value: &str) -> Result<Self, Self::Err> {
    129         Self::parse(value)
    130     }
    131 }
    132 
    133 impl fmt::Display for RadrootsNostrConnectUri {
    134     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    135         match self {
    136             Self::Bunker(uri) => {
    137                 let mut serializer = url::form_urlencoded::Serializer::new(String::new());
    138                 for relay in &uri.relays {
    139                     serializer.append_pair("relay", &relay.to_string());
    140                 }
    141                 if let Some(secret) = &uri.secret {
    142                     serializer.append_pair("secret", secret);
    143                 }
    144                 let query = serializer.finish();
    145                 write!(
    146                     f,
    147                     "{RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME}://{}?{query}",
    148                     uri.remote_signer_public_key
    149                 )
    150             }
    151             Self::Client(uri) => {
    152                 let mut serializer = url::form_urlencoded::Serializer::new(String::new());
    153                 for relay in &uri.relays {
    154                     serializer.append_pair("relay", &relay.to_string());
    155                 }
    156                 serializer.append_pair("secret", &uri.secret);
    157                 if !uri.metadata.requested_permissions.is_empty() {
    158                     serializer
    159                         .append_pair("perms", &uri.metadata.requested_permissions.to_string());
    160                 }
    161                 if let Some(name) = &uri.metadata.name {
    162                     serializer.append_pair("name", name);
    163                 }
    164                 if let Some(url) = &uri.metadata.url {
    165                     serializer.append_pair("url", url);
    166                 }
    167                 if let Some(image) = &uri.metadata.image {
    168                     serializer.append_pair("image", image);
    169                 }
    170                 let query = serializer.finish();
    171                 write!(
    172                     f,
    173                     "{RADROOTS_NOSTR_CONNECT_URI_SCHEME}://{}?{query}",
    174                     uri.client_public_key
    175                 )
    176             }
    177         }
    178     }
    179 }
    180 
    181 fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> {
    182     PublicKey::parse(value)
    183         .or_else(|_| PublicKey::from_hex(value))
    184         .map_err(|error| RadrootsNostrConnectError::InvalidPublicKey {
    185             value: value.to_owned(),
    186             reason: error.to_string(),
    187         })
    188 }
    189 
    190 fn parse_relay_url(value: &str) -> Result<RelayUrl, RadrootsNostrConnectError> {
    191     RelayUrl::parse(value).map_err(|error| RadrootsNostrConnectError::InvalidRelayUrl {
    192         value: value.to_owned(),
    193         reason: error.to_string(),
    194     })
    195 }
    196 
    197 fn validate_url(value: &str) -> Result<String, RadrootsNostrConnectError> {
    198     Url::parse(value)
    199         .map(|url| url.to_string())
    200         .map_err(|error| RadrootsNostrConnectError::InvalidUrl {
    201             value: value.to_owned(),
    202             reason: error.to_string(),
    203         })
    204 }