nostr_types/types/
profile.rs1use super::{PublicKey, UncheckedUrl};
2use crate::Error;
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "speedy")]
5use speedy::{Readable, Writable};
6
7#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
10pub struct Profile {
11 pub pubkey: PublicKey,
13
14 pub relays: Vec<UncheckedUrl>,
16}
17
18impl Profile {
19 pub fn as_bech32_string(&self) -> String {
21 let mut tlv: Vec<u8> = Vec::new();
23
24 tlv.push(0); tlv.push(32); tlv.extend(self.pubkey.as_slice());
28
29 for relay in &self.relays {
31 tlv.push(1); let len = relay.0.len() as u8;
33 tlv.push(len); tlv.extend(&relay.0.as_bytes()[..len as usize]);
35 }
36
37 bech32::encode::<bech32::Bech32>(*crate::HRP_NPROFILE, &tlv).unwrap()
38 }
39
40 pub fn try_from_bech32_string(s: &str, verify: bool) -> Result<Profile, Error> {
45 let data = bech32::decode(s)?;
46 if data.0 != *crate::HRP_NPROFILE {
47 Err(Error::WrongBech32(
48 crate::HRP_NPROFILE.to_lowercase(),
49 data.0.to_lowercase(),
50 ))
51 } else {
52 let mut relays: Vec<UncheckedUrl> = Vec::new();
53 let mut pubkey: Option<PublicKey> = None;
54 let tlv = data.1;
55 let mut pos = 0;
56 loop {
57 if pos > tlv.len() - 2 {
59 break;
60 }
61 let ty = tlv[pos];
62 let len = tlv[pos + 1] as usize;
63 pos += 2;
64 if pos + len > tlv.len() {
65 return Err(Error::InvalidProfile);
66 }
67 match ty {
68 0 => {
69 if len != 32 {
71 return Err(Error::InvalidProfile);
72 }
73 pubkey = Some(PublicKey::from_bytes(&tlv[pos..pos + len], verify)?);
74 }
75 1 => {
76 let relay_bytes = &tlv[pos..pos + len];
78 let relay_str = std::str::from_utf8(relay_bytes)?;
79 let relay = UncheckedUrl::from_str(relay_str);
80 relays.push(relay);
81 }
82 _ => {} }
84 pos += len;
85 }
86 if let Some(pubkey) = pubkey {
87 Ok(Profile { pubkey, relays })
88 } else {
89 Err(Error::InvalidProfile)
90 }
91 }
92 }
93
94 #[allow(dead_code)]
96 pub(crate) fn mock() -> Profile {
97 let pubkey = PublicKey::try_from_hex_string(
98 "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9",
99 true,
100 )
101 .unwrap();
102
103 Profile {
104 pubkey,
105 relays: vec![
106 UncheckedUrl::from_str("wss://relay.example.com"),
107 UncheckedUrl::from_str("wss://relay2.example.com"),
108 ],
109 }
110 }
111}
112
113#[cfg(test)]
114mod test {
115 use super::*;
116
117 test_serde! {Profile, test_profile_serde}
118
119 #[test]
120 fn test_profile_bech32() {
121 let bech32 = Profile::mock().as_bech32_string();
122 println!("{bech32}");
123 assert_eq!(
124 Profile::mock(),
125 Profile::try_from_bech32_string(&bech32, true).unwrap()
126 );
127 }
128
129 #[test]
130 fn test_nip19_example() {
131 let profile = Profile {
132 pubkey: PublicKey::try_from_hex_string(
133 "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
134 true,
135 )
136 .unwrap(),
137 relays: vec![
138 UncheckedUrl::from_str("wss://r.x.com"),
139 UncheckedUrl::from_str("wss://djbas.sadkb.com"),
140 ],
141 };
142
143 let bech32 = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p";
144
145 assert_eq!(profile.as_bech32_string(), bech32);
147
148 assert_eq!(
150 profile,
151 Profile::try_from_bech32_string(bech32, true).unwrap()
152 );
153 }
154}