use anyhow::Result; use base64::Engine; use log::info; use nostr_sdk::{serde_json, EventBuilder, JsonUtil, Kind, NostrSigner, Tag, Timestamp}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::io::SeekFrom; use std::ops::Add; use std::path::PathBuf; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncSeekExt}; use url::Url; #[derive(Debug, Clone)] pub struct Blossom { url: Url, client: reqwest::Client, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BlobDescriptor { pub url: String, pub sha256: String, pub size: u64, #[serde(rename = "type", skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub uploaded: u64, #[serde(rename = "nip94", skip_serializing_if = "Option::is_none")] pub nip94: Option>, } impl Blossom { pub fn new(url: &str) -> Self { Self { url: url.parse().unwrap(), client: reqwest::Client::new(), } } async fn hash_file(f: &mut File) -> Result { let mut hash = Sha256::new(); let mut buf: [u8; 1024] = [0; 1024]; f.seek(SeekFrom::Start(0)).await?; while let Ok(data) = f.read(&mut buf[..]).await { if data == 0 { break; } hash.update(&buf[..data]); } let hash = hash.finalize(); f.seek(SeekFrom::Start(0)).await?; Ok(hex::encode(hash)) } pub async fn upload( &self, from_file: &PathBuf, keys: &S, mime: Option<&str>, ) -> Result where S: NostrSigner, { let mut f = File::open(from_file).await?; let hash = Self::hash_file(&mut f).await?; let auth_event = EventBuilder::new(Kind::Custom(24242), "Upload blob").tags([ Tag::hashtag("upload"), Tag::parse(["x", &hash])?, Tag::expiration(Timestamp::now().add(60)), ]); let auth_event = auth_event.sign(keys).await?; let rsp = self .client .put(self.url.join("/upload").unwrap()) .header("Content-Type", mime.unwrap_or("application/octet-stream")) .header( "Authorization", &format!( "Nostr {}", base64::engine::general_purpose::STANDARD .encode(auth_event.as_json().as_bytes()) ), ) .body(f) .send() .await? .text() .await?; info!("Upload response: {}", rsp); Ok(serde_json::from_str::(&rsp)?) } }