1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// Copyright (c) 2022 Thomas (0xtlt)
// Copyright (c) 2022-2023 Yuki Kishimoto
// Distributed under the MIT software license

//! NIP11
//!
//! <https://github.com/nostr-protocol/nips/blob/master/11.md>

use core::fmt;
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;

#[cfg(not(target_arch = "wasm32"))]
use reqwest::Proxy;
use serde::{Deserialize, Serialize};
use url::Url;

/// `NIP11` error
#[derive(Debug)]
pub enum Error {
    /// The relay information document is invalid
    InvalidInformationDocument,
    /// The relay information document is not accessible
    InaccessibleInformationDocument,
    /// Provided URL scheme is not valid
    InvalidScheme,
    /// Reqwest error
    Reqwest(reqwest::Error),
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidInformationDocument => {
                write!(f, "The relay information document is invalid")
            }
            Self::InaccessibleInformationDocument => {
                write!(f, "The relay information document is not accessible")
            }
            Self::InvalidScheme => write!(f, "Provided URL scheme is not valid"),
            Self::Reqwest(e) => write!(f, "{e}"),
        }
    }
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Self::Reqwest(e)
    }
}

/// Relay information document
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RelayInformationDocument {
    /// Name
    pub name: Option<String>,
    /// Description
    pub description: Option<String>,
    /// Owner public key
    pub pubkey: Option<String>,
    /// Owner contact
    pub contact: Option<String>,
    /// Supported NIPs
    pub supported_nips: Option<Vec<u16>>,
    /// Software
    pub software: Option<String>,
    /// Software version
    pub version: Option<String>,
}

impl RelayInformationDocument {
    /// Create new empty [`RelayInformationDocument`]
    pub fn new() -> Self {
        Self::default()
    }

    /// Get Relay Information Document
    #[cfg(not(target_arch = "wasm32"))]
    pub async fn get(url: Url, proxy: Option<SocketAddr>) -> Result<Self, Error> {
        use reqwest::Client;

        let mut builder = Client::builder();
        if let Some(proxy) = proxy {
            let proxy = format!("socks5h://{proxy}");
            builder = builder.proxy(Proxy::all(proxy)?);
        }
        let client: Client = builder.build()?;
        let url = Self::with_http_scheme(url)?;
        let req = client.get(url).header("Accept", "application/nostr+json");
        match req.send().await {
            Ok(response) => match response.json().await {
                Ok(json) => Ok(json),
                Err(_) => Err(Error::InvalidInformationDocument),
            },
            Err(_) => Err(Error::InaccessibleInformationDocument),
        }
    }

    /// Get Relay Information Document
    #[cfg(not(target_arch = "wasm32"))]
    #[cfg(feature = "blocking")]
    pub fn get_blocking(url: Url, proxy: Option<SocketAddr>) -> Result<Self, Error> {
        use reqwest::blocking::Client;

        let mut builder = Client::builder();
        if let Some(proxy) = proxy {
            let proxy = format!("socks5h://{proxy}");
            builder = builder.proxy(Proxy::all(proxy)?);
        }
        let client: Client = builder.build()?;
        let url = Self::with_http_scheme(url)?;
        let req = client.get(url).header("Accept", "application/nostr+json");
        match req.send() {
            Ok(response) => match response.json() {
                Ok(json) => Ok(json),
                Err(_) => Err(Error::InvalidInformationDocument),
            },
            Err(_) => Err(Error::InaccessibleInformationDocument),
        }
    }

    /// Get Relay Information Document
    #[cfg(target_arch = "wasm32")]
    pub async fn get(url: Url) -> Result<Self, Error> {
        use reqwest::Client;

        let client: Client = Client::new();
        let url = Self::with_http_scheme(url)?;
        let req = client.get(url).header("Accept", "application/nostr+json");
        match req.send().await {
            Ok(response) => match response.json().await {
                Ok(json) => Ok(json),
                Err(_) => Err(Error::InvalidInformationDocument),
            },
            Err(_) => Err(Error::InaccessibleInformationDocument),
        }
    }

    /// Returns new URL with scheme substituted to HTTP(S) if WS(S) was provided,
    /// other schemes leaves untouched.
    fn with_http_scheme(url: Url) -> Result<Url, Error> {
        let mut url = url;
        match url.scheme() {
            "wss" => url.set_scheme("https").map_err(|_| Error::InvalidScheme)?,
            "ws" => url.set_scheme("http").map_err(|_| Error::InvalidScheme)?,
            _ => {}
        }
        Ok(url)
    }
}