nostr_types/
lib.rs

1// Copyright 2015-2020 nostr-proto Developers
2// Licensed under the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>
3// This file may not be copied, modified, or distributed except according to those terms.
4
5//! This crate provides types for nostr protocol handling.
6
7#![deny(
8    missing_debug_implementations,
9    trivial_casts,
10    trivial_numeric_casts,
11    unused_import_braces,
12    //unused_qualifications,
13    unused_results,
14    unused_lifetimes,
15    unused_labels,
16    unused_extern_crates,
17    non_ascii_idents,
18    keyword_idents,
19    deprecated_in_future,
20    unstable_features,
21    single_use_lifetimes,
22    //unsafe_code,
23    unreachable_pub,
24    missing_docs,
25    missing_copy_implementations
26)]
27#![deny(clippy::string_slice)]
28
29mod error;
30pub use error::Error;
31
32#[cfg(test)]
33macro_rules! test_serde {
34    ($t:ty, $fnname:ident) => {
35        #[test]
36        fn $fnname() {
37            let a = <$t>::mock();
38            let x = serde_json::to_string(&a).unwrap();
39            println!("{}", x);
40            let b = serde_json::from_str(&x).unwrap();
41            assert_eq!(a, b);
42        }
43    };
44}
45
46#[cfg(test)]
47macro_rules! test_serde_async {
48    ($t:ty, $fnname:ident) => {
49        #[tokio::test]
50        async fn $fnname() {
51            let a = <$t>::mock().await;
52            let x = serde_json::to_string(&a).unwrap();
53            println!("{}", x);
54            let b = serde_json::from_str(&x).unwrap();
55            assert_eq!(a, b);
56        }
57    };
58}
59
60#[cfg(test)]
61macro_rules! test_serde_val {
62    ($fnname:ident, $val:expr) => {
63        #[test]
64        fn $fnname() {
65            let a = $val;
66            let x = serde_json::to_string(&a).unwrap();
67            println!("{}", x);
68            let b = serde_json::from_str(&x).unwrap();
69            assert_eq!(a, b);
70        }
71    };
72}
73
74#[cfg(test)]
75macro_rules! test_serde_val_async {
76    ($fnname:ident, $val:expr) => {
77        #[tokio::test]
78        async fn $fnname() {
79            let a = $val;
80            let x = serde_json::to_string(&a).unwrap();
81            println!("{}", x);
82            let b = serde_json::from_str(&x).unwrap();
83            assert_eq!(a, b);
84        }
85    };
86}
87
88/// A basic nostr client
89#[cfg(feature = "client")]
90pub mod client;
91
92/// NIP-46 nsec bunker related types and functions
93#[cfg(feature = "nip46")]
94pub mod nip46;
95
96mod types;
97pub use types::{
98    find_nostr_bech32_pos, find_nostr_url_pos, ClientMessage, ContentEncryptionAlgorithm,
99    ContentSegment, CountResult, DelegationConditions, EncryptedPrivateKey, Event, EventDelegation,
100    EventKind, EventKindIterator, EventKindOrRange, EventReference, ExportableSigner, Fee,
101    FileMetadata, Filter, Hll8, Id, IdHex, Identity, KeySecurity, KeySigner, LockableSigner,
102    Metadata, MilliSatoshi, MutExportableSigner, NAddr, NEvent, Nip05, NostrBech32, NostrUrl,
103    ParsedTag, PayRequestData, PreEvent, PrivateKey, Profile, PublicKey, PublicKeyHex, RelayFees,
104    RelayInformationDocument, RelayLimitation, RelayList, RelayListUsage, RelayMessage,
105    RelayOrigin, RelayRetention, RelayUrl, RelayUsage, RelayUsageSet, Rumor, ShatteredContent,
106    Signature, SignatureHex, Signer, SignerExt, SimpleRelayList, SimpleRelayUsage, Span,
107    SubscriptionId, Tag, UncheckedUrl, Unixtime, Url, Why, XOnlyPublicKey, ZapData,
108};
109
110mod versioned;
111pub use versioned::{
112    EventV3, FeeV1, FilterV1, FilterV2, MetadataV1, MetadataV2, Nip05V1, PreEventV3, RelayFeesV1,
113    RelayInformationDocumentV1, RelayInformationDocumentV2, RelayLimitationV1, RelayLimitationV2,
114    RelayRetentionV1, RumorV3, TagV3, ZapDataV1, ZapDataV2,
115};
116
117#[inline]
118pub(crate) fn get_leading_zero_bits(bytes: &[u8]) -> u8 {
119    let mut res = 0_u8;
120    for b in bytes {
121        if *b == 0 {
122            res += 8;
123        } else {
124            res += b.leading_zeros() as u8;
125            return res;
126        }
127    }
128    res
129}
130
131trait IntoVec<T> {
132    fn into_vec(self) -> Vec<T>;
133}
134
135impl<T> IntoVec<T> for Option<T> {
136    fn into_vec(self) -> Vec<T> {
137        match self {
138            None => vec![],
139            Some(t) => vec![t],
140        }
141    }
142}
143
144use bech32::Hrp;
145lazy_static::lazy_static! {
146    static ref HRP_LNURL: Hrp = Hrp::parse("lnurl").expect("HRP error on lnurl");
147    static ref HRP_NADDR: Hrp = Hrp::parse("naddr").expect("HRP error on naddr");
148    static ref HRP_NCRYPTSEC: Hrp = Hrp::parse("ncryptsec").expect("HRP error on ncryptsec");
149    static ref HRP_NEVENT: Hrp = Hrp::parse("nevent").expect("HRP error on nevent");
150    static ref HRP_NOTE: Hrp = Hrp::parse("note").expect("HRP error on note");
151    static ref HRP_NPROFILE: Hrp = Hrp::parse("nprofile").expect("HRP error on nprofile");
152    static ref HRP_NPUB: Hrp = Hrp::parse("npub").expect("HRP error on npub");
153    static ref HRP_NRELAY: Hrp = Hrp::parse("nrelay").expect("HRP error on nrelay");
154    static ref HRP_NSEC: Hrp = Hrp::parse("nsec").expect("HRP error on nsec");
155}
156
157/// Add a 'p' pubkey tag to a set of tags if it doesn't already exist
158pub fn add_pubkey_to_tags(
159    existing_tags: &mut Vec<Tag>,
160    new_pubkey: PublicKey,
161    new_hint: Option<UncheckedUrl>,
162) -> usize {
163    let index = existing_tags.iter().position(|existing_tag| {
164        if let Ok(ParsedTag::Pubkey { pubkey, .. }) = existing_tag.parse() {
165            pubkey == new_pubkey
166        } else {
167            false
168        }
169    });
170
171    if let Some(idx) = index {
172        // force additional data to match
173        existing_tags[idx].set_index(
174            2,
175            match new_hint {
176                Some(u) => u.as_str().to_owned(),
177                None => "".to_owned(),
178            },
179        );
180        existing_tags[idx].trim();
181        idx
182    } else {
183        existing_tags.push(
184            ParsedTag::Pubkey {
185                pubkey: new_pubkey,
186                recommended_relay_url: new_hint,
187                petname: None,
188            }
189            .into_tag(),
190        );
191        existing_tags.len() - 1
192    }
193}
194
195/// Add an 'e' id tag to a set of tags if it doesn't already exist
196pub fn add_event_to_tags(
197    existing_tags: &mut Vec<Tag>,
198    new_id: Id,
199    new_hint: Option<UncheckedUrl>,
200    new_marker: &str,
201    new_pubkey: Option<PublicKey>,
202    use_quote: bool,
203) -> usize {
204    // NIP-18: "Quote reposts are kind 1 events with an embedded q tag..."
205    if new_marker == "mention" && use_quote {
206        let index = existing_tags.iter().position(|existing_tag| {
207            if let Ok(ParsedTag::Quote { id, .. }) = existing_tag.parse() {
208                id == new_id
209            } else {
210                false
211            }
212        });
213
214        if let Some(idx) = index {
215            // force additional data to match
216            existing_tags[idx].set_index(
217                2,
218                match new_hint {
219                    Some(u) => u.as_str().to_owned(),
220                    None => "".to_owned(),
221                },
222            );
223            existing_tags[idx].set_index(
224                3,
225                match new_pubkey {
226                    Some(pk) => pk.as_hex_string(),
227                    None => "".to_owned(),
228                },
229            );
230            existing_tags[idx].trim();
231            idx
232        } else {
233            let newtag = ParsedTag::Quote {
234                id: new_id,
235                recommended_relay_url: new_hint,
236                author_pubkey: new_pubkey,
237            }
238            .into_tag();
239            existing_tags.push(newtag);
240            existing_tags.len() - 1
241        }
242    } else {
243        let index = existing_tags.iter().position(|existing_tag| {
244            if let Ok(ParsedTag::Event { id, .. }) = existing_tag.parse() {
245                id == new_id
246            } else {
247                false
248            }
249        });
250
251        if let Some(idx) = index {
252            // force additional data to match
253            existing_tags[idx].set_index(
254                2,
255                match new_hint {
256                    Some(u) => u.as_str().to_owned(),
257                    None => "".to_owned(),
258                },
259            );
260            existing_tags[idx].set_index(3, new_marker.to_owned());
261            existing_tags[idx].set_index(
262                4,
263                match new_pubkey {
264                    Some(pk) => pk.as_hex_string(),
265                    None => "".to_owned(),
266                },
267            );
268            existing_tags[idx].trim();
269            idx
270        } else {
271            let newtag = ParsedTag::Event {
272                id: new_id,
273                recommended_relay_url: new_hint,
274                marker: Some(new_marker.to_string()),
275                author_pubkey: new_pubkey,
276            }
277            .into_tag();
278            existing_tags.push(newtag);
279            existing_tags.len() - 1
280        }
281    }
282}
283
284/// Add an 'a' addr tag to a set of tags if it doesn't already exist
285pub fn add_addr_to_tags(
286    existing_tags: &mut Vec<Tag>,
287    new_addr: &NAddr,
288    new_marker: Option<String>,
289) -> usize {
290    let index = existing_tags.iter().position(|existing_tag| {
291        if let Ok(ParsedTag::Address { address, .. }) = existing_tag.parse() {
292            address.kind == new_addr.kind
293                && address.author == new_addr.author
294                && address.d == new_addr.d
295        } else {
296            false
297        }
298    });
299
300    if let Some(idx) = index {
301        // force additional data to match
302        existing_tags[idx].set_index(
303            2,
304            match new_marker {
305                Some(s) => s,
306                None => "".to_owned(),
307            },
308        );
309        existing_tags[idx].trim();
310        idx
311    } else {
312        existing_tags.push(
313            ParsedTag::Address {
314                address: new_addr.clone(),
315                marker: new_marker,
316            }
317            .into_tag(),
318        );
319        existing_tags.len() - 1
320    }
321}
322
323/// Add an 'subject' tag to a set of tags if it doesn't already exist
324pub fn add_subject_to_tags_if_missing(existing_tags: &mut Vec<Tag>, subject: String) {
325    if !existing_tags.iter().any(|t| t.tagname() == "subject") {
326        existing_tags.push(ParsedTag::Subject(subject).into_tag());
327    }
328}