nostr_types/types/
profile.rs

1use super::{PublicKey, UncheckedUrl};
2use crate::Error;
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "speedy")]
5use speedy::{Readable, Writable};
6
7/// A person's profile on nostr which consists of the data needed in order to follow someone.
8#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
10pub struct Profile {
11    /// Their public key
12    pub pubkey: PublicKey,
13
14    /// Some of the relays they post to (when the profile was created)
15    pub relays: Vec<UncheckedUrl>,
16}
17
18impl Profile {
19    /// Export as a bech32 encoded string ("nprofile")
20    pub fn as_bech32_string(&self) -> String {
21        // Compose
22        let mut tlv: Vec<u8> = Vec::new();
23
24        // Push Public Key
25        tlv.push(0); // the special value, in this case the public key
26        tlv.push(32); // the length of the value (always 32 for public key)
27        tlv.extend(self.pubkey.as_slice());
28
29        // Push relays
30        for relay in &self.relays {
31            tlv.push(1); // type 'relay'
32            let len = relay.0.len() as u8;
33            tlv.push(len); // the length of the string
34            tlv.extend(&relay.0.as_bytes()[..len as usize]);
35        }
36
37        bech32::encode::<bech32::Bech32>(*crate::HRP_NPROFILE, &tlv).unwrap()
38    }
39
40    /// Import from a bech32 encoded string ("nprofile")
41    ///
42    /// If verify is true, will verify that it works as a secp256k1::XOnlyPublicKey. This
43    /// has a performance cost.
44    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                // we need at least 2 more characters for anything meaningful
58                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                        // special,  32 bytes of the public key
70                        if len != 32 {
71                            return Err(Error::InvalidProfile);
72                        }
73                        pubkey = Some(PublicKey::from_bytes(&tlv[pos..pos + len], verify)?);
74                    }
75                    1 => {
76                        // relay
77                        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                    _ => {} // unhandled type for nprofile
83                }
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    // Mock data for testing
95    #[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        // Try converting profile to bech32
146        assert_eq!(profile.as_bech32_string(), bech32);
147
148        // Try converting bech32 to profile
149        assert_eq!(
150            profile,
151            Profile::try_from_bech32_string(bech32, true).unwrap()
152        );
153    }
154}