use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
use reqwest::Proxy;
use crate::{Timestamp, Url};
#[derive(Debug)]
pub enum Error {
InvalidInformationDocument,
InaccessibleInformationDocument,
InvalidScheme,
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)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RelayInformationDocument {
pub name: Option<String>,
pub description: Option<String>,
pub pubkey: Option<String>,
pub contact: Option<String>,
pub supported_nips: Option<Vec<u16>>,
pub software: Option<String>,
pub version: Option<String>,
pub limitation: Option<Limitation>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub retention: Vec<Retention>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub relay_countries: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub language_tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub tags: Vec<String>,
pub posting_policy: Option<String>,
pub payments_url: Option<String>,
pub fees: Option<FeeSchedules>,
pub icon: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Limitation {
pub max_message_length: Option<i32>,
pub max_subscriptions: Option<i32>,
pub max_filters: Option<i32>,
pub max_limit: Option<i32>,
pub max_subid_length: Option<i32>,
pub max_event_tags: Option<i32>,
pub max_content_length: Option<i32>,
pub min_pow_difficulty: Option<i32>,
pub auth_required: Option<bool>,
pub payment_required: Option<bool>,
pub created_at_lower_limit: Option<Timestamp>,
pub created_at_upper_limit: Option<Timestamp>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Retention {
pub kinds: Option<Vec<RetentionKind>>,
pub time: Option<u64>,
pub count: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RetentionKind {
Single(u64),
Range(u64, u64),
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FeeSchedules {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub admission: Vec<FeeSchedule>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub subscription: Vec<FeeSchedule>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub publication: Vec<FeeSchedule>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FeeSchedule {
pub amount: i32,
pub unit: String,
pub period: Option<i32>,
pub kinds: Option<Vec<String>>,
}
impl RelayInformationDocument {
pub fn new() -> Self {
Self::default()
}
pub async fn get(url: Url, _proxy: Option<SocketAddr>) -> Result<Self, Error> {
use reqwest::Client;
#[cfg(not(target_arch = "wasm32"))]
let client: Client = {
let mut builder = Client::builder();
if let Some(proxy) = _proxy {
let proxy = format!("socks5h://{proxy}");
builder = builder.proxy(Proxy::all(proxy)?);
}
builder.build()?
};
#[cfg(target_arch = "wasm32")]
let client: Client = Client::new();
let url = Self::with_http_scheme(url)?;
let req = client
.get(url.to_string())
.header("Accept", "application/nostr+json");
match req.send().await {
Ok(response) => {
let json: String = response.text().await?;
tracing::debug!("Response: {json}");
match serde_json::from_slice(json.as_bytes()) {
Ok(json) => Ok(json),
Err(_) => Err(Error::InvalidInformationDocument),
}
}
Err(_) => Err(Error::InaccessibleInformationDocument),
}
}
#[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.to_string())
.header("Accept", "application/nostr+json");
match req.send() {
Ok(response) => {
let json: String = response.text()?;
tracing::debug!("Response: {json}");
match serde_json::from_slice(json.as_bytes()) {
Ok(json) => Ok(json),
Err(_) => Err(Error::InvalidInformationDocument),
}
}
Err(_) => Err(Error::InaccessibleInformationDocument),
}
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn correctly_serializes_retention_kind() {
let kinds = vec![
RetentionKind::Single(0),
RetentionKind::Single(1),
RetentionKind::Range(5, 7),
RetentionKind::Range(40, 49),
];
let got = serde_json::to_string(&kinds).unwrap();
let expected = "[0,1,[5,7],[40,49]]".to_string();
assert!(got == expected, "got: {}, expected: {}", got, expected);
}
#[test]
fn correctly_deserializes_retention_kind() {
let kinds = "[0, 1, [5, 7], [40, 49]]";
let got = serde_json::from_str::<Vec<RetentionKind>>(kinds).unwrap();
let expected = vec![
RetentionKind::Single(0),
RetentionKind::Single(1),
RetentionKind::Range(5, 7),
RetentionKind::Range(40, 49),
];
assert!(got == expected, "got: {:?}, expected: {:?}", got, expected);
}
}