210 lines
7.2 KiB
Rust
210 lines
7.2 KiB
Rust
//! NIP-19 decoding and Nostr URI normalization.
|
|
//!
|
|
//! Port of `nostr_url.h` / `nostr_url.c` from the C project.
|
|
|
|
use nostr_core::NostrResult;
|
|
use std::fmt;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum NostrEntityType {
|
|
None,
|
|
Npub,
|
|
Nsec,
|
|
Note,
|
|
Nevent,
|
|
Naddr,
|
|
Nprofile,
|
|
Nrelay,
|
|
}
|
|
|
|
impl fmt::Display for NostrEntityType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
NostrEntityType::None => write!(f, "none"),
|
|
NostrEntityType::Npub => write!(f, "npub"),
|
|
NostrEntityType::Nsec => write!(f, "nsec"),
|
|
NostrEntityType::Note => write!(f, "note"),
|
|
NostrEntityType::Nevent => write!(f, "nevent"),
|
|
NostrEntityType::Naddr => write!(f, "naddr"),
|
|
NostrEntityType::Nprofile => write!(f, "nprofile"),
|
|
NostrEntityType::Nrelay => write!(f, "nrelay"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct NostrDecodedEntity {
|
|
pub entity_type: NostrEntityType,
|
|
pub pubkey: Option<[u8; 32]>,
|
|
pub event_id: Option<[u8; 32]>,
|
|
pub kind: Option<u32>,
|
|
pub identifier: Option<String>,
|
|
pub relay_url: Option<String>,
|
|
pub relay_hints: Vec<String>,
|
|
}
|
|
|
|
impl Default for NostrDecodedEntity {
|
|
fn default() -> Self {
|
|
NostrDecodedEntity {
|
|
entity_type: NostrEntityType::None,
|
|
pubkey: None,
|
|
event_id: None,
|
|
kind: None,
|
|
identifier: None,
|
|
relay_url: None,
|
|
relay_hints: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn nostr_url_detect(input: &str) -> NostrEntityType {
|
|
if input.starts_with("npub") { NostrEntityType::Npub }
|
|
else if input.starts_with("nsec") { NostrEntityType::Nsec }
|
|
else if input.starts_with("note") { NostrEntityType::Note }
|
|
else if input.starts_with("nevent") { NostrEntityType::Nevent }
|
|
else if input.starts_with("naddr") { NostrEntityType::Naddr }
|
|
else if input.starts_with("nprofile") { NostrEntityType::Nprofile }
|
|
else if input.starts_with("nrelay") { NostrEntityType::Nrelay }
|
|
else { NostrEntityType::None }
|
|
}
|
|
|
|
pub fn nostr_url_decode(input: &str) -> NostrResult<NostrDecodedEntity> {
|
|
let entity_type = nostr_url_detect(input);
|
|
if entity_type == NostrEntityType::None {
|
|
return Err(nostr_core::NostrError::InvalidInput);
|
|
}
|
|
|
|
// bech32 0.11.1 decode returns (Hrp, Vec<u8>)
|
|
let (_hrp, bytes): (bech32::Hrp, Vec<u8>) = bech32::decode(input)
|
|
.map_err(|_| nostr_core::NostrError::InvalidInput)?;
|
|
|
|
let mut entity = NostrDecodedEntity {
|
|
entity_type,
|
|
..Default::default()
|
|
};
|
|
|
|
match entity_type {
|
|
NostrEntityType::Npub | NostrEntityType::Nsec => {
|
|
if bytes.len() == 32 {
|
|
let mut arr = [0u8; 32];
|
|
arr.copy_from_slice(&bytes);
|
|
entity.pubkey = Some(arr);
|
|
}
|
|
}
|
|
NostrEntityType::Note => {
|
|
if bytes.len() == 32 {
|
|
let mut arr = [0u8; 32];
|
|
arr.copy_from_slice(&bytes);
|
|
entity.event_id = Some(arr);
|
|
}
|
|
}
|
|
NostrEntityType::Nevent | NostrEntityType::Nprofile | NostrEntityType::Naddr => {
|
|
let mut i = 0;
|
|
while i < bytes.len() {
|
|
if i + 1 >= bytes.len() { break; }
|
|
let tlv_type = bytes[i];
|
|
let tlv_len = bytes[i + 1] as usize;
|
|
i += 2;
|
|
if i + tlv_len > bytes.len() { break; }
|
|
let tlv_value = &bytes[i..i + tlv_len];
|
|
i += tlv_len;
|
|
match tlv_type {
|
|
0 => {
|
|
if tlv_value.len() == 32 {
|
|
let mut arr = [0u8; 32];
|
|
arr.copy_from_slice(tlv_value);
|
|
if entity_type == NostrEntityType::Nevent {
|
|
entity.event_id = Some(arr);
|
|
} else {
|
|
entity.pubkey = Some(arr);
|
|
}
|
|
}
|
|
}
|
|
1 => { if let Ok(s) = String::from_utf8(tlv_value.to_vec()) { entity.relay_hints.push(s); } }
|
|
2 => { if entity_type == NostrEntityType::Naddr { if let Ok(s) = String::from_utf8(tlv_value.to_vec()) { entity.identifier = Some(s); } } }
|
|
3 => { if tlv_value.len() == 4 { let mut arr = [0u8; 4]; arr.copy_from_slice(tlv_value); entity.kind = Some(u32::from_be_bytes(arr)); } }
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
NostrEntityType::Nrelay => {
|
|
if let Ok(s) = String::from_utf8(bytes) { entity.relay_url = Some(s); }
|
|
}
|
|
NostrEntityType::None => {}
|
|
}
|
|
|
|
Ok(entity)
|
|
}
|
|
|
|
pub fn nostr_url_normalize(input: &str) -> String {
|
|
if let Ok(entity) = nostr_url_decode(input) {
|
|
if let Some(pk) = entity.pubkey { return hex::encode(pk); }
|
|
}
|
|
input.to_string()
|
|
}
|
|
|
|
pub fn nostr_url_entity(input: &str) -> Option<&str> {
|
|
if let Some(stripped) = input.strip_prefix("nostr://") {
|
|
Some(stripped)
|
|
} else if let Some(stripped) = input.strip_prefix("nostr:") {
|
|
Some(stripped)
|
|
} else {
|
|
Some(input)
|
|
}
|
|
}
|
|
|
|
pub fn nostr_hex_encode32(data: &[u8; 32]) -> String {
|
|
hex::encode(data)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_nostr_url_detect() {
|
|
assert_eq!(nostr_url_detect("npub1abc"), NostrEntityType::Npub);
|
|
assert_eq!(nostr_url_detect("nsec1abc"), NostrEntityType::Nsec);
|
|
assert_eq!(nostr_url_detect("note1abc"), NostrEntityType::Note);
|
|
assert_eq!(nostr_url_detect("nevent1abc"), NostrEntityType::Nevent);
|
|
assert_eq!(nostr_url_detect("naddr1abc"), NostrEntityType::Naddr);
|
|
assert_eq!(nostr_url_detect("nprofile1abc"), NostrEntityType::Nprofile);
|
|
assert_eq!(nostr_url_detect("nrelay1abc"), NostrEntityType::Nrelay);
|
|
assert_eq!(nostr_url_detect("unknown"), NostrEntityType::None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nostr_url_entity() {
|
|
assert_eq!(nostr_url_entity("nostr:npub1abc"), Some("npub1abc"));
|
|
assert_eq!(nostr_url_entity("nostr://npub1abc"), Some("npub1abc"));
|
|
assert_eq!(nostr_url_entity("npub1abc"), Some("npub1abc"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_nostr_hex_encode32() {
|
|
let data = [0u8; 32];
|
|
assert_eq!(nostr_hex_encode32(&data), "0000000000000000000000000000000000000000000000000000000000000000");
|
|
|
|
let data = [0xffu8; 32];
|
|
assert_eq!(nostr_hex_encode32(&data), "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_npub() {
|
|
// Test with a known valid npub
|
|
// npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m
|
|
// corresponds to pubkey: 82341f2f8f1b1e1e6c0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e
|
|
let result = nostr_url_decode("npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m");
|
|
assert!(result.is_ok());
|
|
let entity = result.unwrap();
|
|
assert_eq!(entity.entity_type, NostrEntityType::Npub);
|
|
assert!(entity.pubkey.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_decode_invalid() {
|
|
let result = nostr_url_decode("invalid");
|
|
assert!(result.is_err());
|
|
}
|
|
}
|