use crate::Url;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::path::PathBuf;
use reqwest::{Client, multipart};
use tokio::fs;
use bitcoin::hashes::Hash;
use bitcoin::hashes::sha256::Hash as Sha256Hash;
use rand::seq::SliceRandom;
use crate::nips::nip98;
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub api_url: String,
pub download_url: String,
pub delegated_to_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Nip94Event {
tags: Vec<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct UploadResponse {
status: String,
nip94_event: Nip94Event,
}
const NIP96_SERVERS: &[&str] = &[
"https://nostrcheck.me",
"https://nostrage.com",
"https://sove.rent",
"https://nostr.build",
"https://files.sovbit.host",
"https://void.cat",
"https://nostpic.com",
"https://nostr.onch.services",
];
fn random_server_url() -> Url {
if let Some(urls) = NIP96_SERVERS.choose(&mut rand::thread_rng()) {
Url::parse(urls).unwrap()
} else {
Url::parse(NIP96_SERVERS[0]).unwrap()
}
}
pub async fn get_server_desc(
server_url: Url,
) -> Result<ServerConfig, Box<dyn Error>> {
let client = Client::new();
let json_url = server_url.join("/.well-known/nostr/nip96.json")?;
let response = client.get(json_url).send().await?;
if let Ok(config) = response.json::<ServerConfig>().await {
Ok(config)
} else {
Err(Box::from("Cannot get server description"))
}
}
pub async fn get_api_url_for(server_url: Url) -> Result<Url, Box<dyn Error>> {
if let Ok(desc) = get_server_desc(server_url.clone()).await {
if let Ok(url) = Url::parse(desc.api_url.as_str()) {
Ok(url)
} else {
Err(Box::from("Invalid URL"))
}
} else {
Err(Box::from("No desc"))
}
}
pub async fn upload_file(
file_path: PathBuf,
mime_type: String,
api_url: Url,
keys: &Keys,
) -> Result<Url, Box<dyn Error>> {
let client = Client::new();
let contents = fs::read(file_path).await?;
let payload = Sha256Hash::hash(&contents[..]);
let nip98_auth = nip98::build_nip98_auth_event_b64(
keys,
api_url.clone(),
payload,
)
.await;
let some_file = multipart::Part::bytes(contents)
.file_name("test.jpg")
.mime_str(mime_type.as_str())?;
let form = multipart::Form::new().part("file", some_file);
let response = client
.post(api_url)
.header("Authorization", format!("Nostr {}", nip98_auth).as_str())
.multipart(form)
.send()
.await?;
if let Ok(resp) = response.json::<UploadResponse>().await {
for tag in resp.nip94_event.tags.iter() {
match tag[0].as_str() {
"url" => {
return Ok(Url::parse(tag[1].as_str()).unwrap());
}
_ => continue,
}
}
Err(Box::from("Uploaded file URL not found"))
} else {
Err(Box::from("Cannot decode upload response"))
}
}
pub async fn upload_file_data(
server_url: Option<Url>,
contents: Vec<u8>,
mime_type: String,
keys: &Keys,
) -> Result<Url, Box<dyn Error>> {
let base_url = match server_url {
Some(url) => url,
None => random_server_url(),
};
let Ok(api_url) = get_api_url_for(base_url.clone()).await else {
return Err(Box::from("Cannot determine the API URL"));
};
let client = Client::new();
let payload = Sha256Hash::hash(&contents[..]);
let nip98_auth = nip98::build_nip98_auth_event_b64(
keys,
api_url.clone(),
payload,
)
.await;
let some_file = multipart::Part::bytes(contents)
.file_name("filename")
.mime_str(mime_type.as_str())?;
let form = multipart::Form::new().part("file", some_file);
let response = client
.post(api_url)
.header("Authorization", format!("Nostr {}", nip98_auth).as_str())
.multipart(form)
.send()
.await?;
if let Ok(resp) = response.json::<UploadResponse>().await {
for tag in resp.nip94_event.tags.iter() {
match tag[0].as_str() {
"url" => {
return Ok(Url::parse(tag[1].as_str()).unwrap());
}
_ => continue,
}
}
Err(Box::from("Uploaded file URL not found"))
} else {
Err(Box::from("Cannot decode upload response"))
}
}