use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use hashes::sha256::Hash as Sha256Hash;
use hashes::Hash;
use serde::{Deserialize, Serialize};
use crate::nips::nip98;
use crate::nips::nip98::{HttpData, HttpMethod};
use crate::types::Url;
use crate::{JsonUtil, NostrSigner, TagKind, TagStandard, Tags};
#[derive(Debug, PartialEq)]
pub enum Error {
NIP98(nip98::Error),
InvalidURL,
ResponseDecodeError,
UploadError(String),
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NIP98(e) => e.fmt(f),
Self::InvalidURL => f.write_str("Invalid URL"),
Self::ResponseDecodeError => f.write_str("Response decoding error"),
Self::UploadError(e) => f.write_str(e),
}
}
}
impl From<nip98::Error> for Error {
fn from(e: nip98::Error) -> Self {
Self::NIP98(e)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ServerConfig {
pub api_url: Url,
pub download_url: Url,
pub delegated_to_url: Option<Url>,
pub content_types: Option<Vec<String>>,
}
impl JsonUtil for ServerConfig {
type Err = serde_json::Error;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Nip94Event {
pub tags: Tags,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UploadResponseStatus {
Success,
Error,
}
impl UploadResponseStatus {
#[inline]
pub fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct UploadResponse {
pub status: UploadResponseStatus,
pub message: String,
pub nip94_event: Option<Nip94Event>,
}
impl UploadResponse {
pub fn download_url(&self) -> Result<&Url, Error> {
if !self.status.is_success() {
return Err(Error::UploadError(self.message.clone()));
}
let nip94_event: &Nip94Event = self
.nip94_event
.as_ref()
.ok_or(Error::ResponseDecodeError)?;
match nip94_event.tags.find_standardized(TagKind::Url) {
Some(TagStandard::Url(url)) => Ok(url),
_ => Err(Error::ResponseDecodeError),
}
}
}
impl JsonUtil for UploadResponse {
type Err = serde_json::Error;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UploadRequest {
pub url: Url,
pub authorization: String,
}
impl UploadRequest {
pub async fn new<T>(signer: &T, config: &ServerConfig, file_data: &[u8]) -> Result<Self, Error>
where
T: NostrSigner,
{
let payload: Sha256Hash = Sha256Hash::hash(file_data);
let data: HttpData =
HttpData::new(config.api_url.clone(), HttpMethod::POST).payload(payload);
let authorization: String = data.to_authorization(signer).await?;
Ok(Self {
url: config.api_url.clone(),
authorization,
})
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn authorization(&self) -> &str {
&self.authorization
}
}
pub fn get_server_config_url(server_url: &Url) -> Result<Url, Error> {
let json_url = server_url
.join("/.well-known/nostr/nip96.json")
.map_err(|_| Error::InvalidURL)?;
Ok(json_url)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_server_config_url() {
let server_url = Url::parse("https://nostr.media").unwrap();
let config_url = get_server_config_url(&server_url).unwrap();
assert_eq!(
config_url.to_string(),
"https://nostr.media/.well-known/nostr/nip96.json"
);
}
#[test]
fn test_server_config_from_json() {
let json_response = r#"{
"api_url": "https://nostr.media/api/v1/nip96/upload",
"download_url": "https://nostr.media"
}"#;
let config = ServerConfig::from_json(json_response).unwrap();
assert_eq!(
config.api_url.to_string(),
"https://nostr.media/api/v1/nip96/upload"
);
assert_eq!(config.download_url.to_string(), "https://nostr.media/");
}
#[test]
fn test_upload_response_download_url() {
let success_response = r#"{
"status": "success",
"message": "Upload successful",
"nip94_event": {
"tags": [["url", "https://nostr.media/file123.png"]]
}
}"#;
let response = UploadResponse::from_json(success_response).unwrap();
let url = response.download_url().unwrap();
assert_eq!(url.to_string(), "https://nostr.media/file123.png");
let error_response = r#"{
"status": "error",
"message": "File too large"
}"#;
let response = UploadResponse::from_json(error_response).unwrap();
let result = response.download_url();
assert!(result.is_err());
if let Err(Error::UploadError(msg)) = result {
assert_eq!(msg, "File too large");
}
}
}