Skip to content

Commit 263463e

Browse files
committed
blossom: add Error enum
Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent d69bc6f commit 263463e

File tree

4 files changed

+144
-46
lines changed

4 files changed

+144
-46
lines changed

crates/nostr-blossom/src/client.rs

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
//! Implements a Blossom client for interacting with Blossom servers
22
3-
use std::error::Error;
43
use std::time::Duration;
54

5+
use base64::engine::general_purpose;
66
use base64::Engine;
77
use nostr::hashes::sha256::Hash as Sha256Hash;
88
use nostr::hashes::Hash;
99
use nostr::signer::NostrSigner;
10-
use nostr::{EventBuilder, PublicKey, Timestamp};
10+
use nostr::{Event, EventBuilder, PublicKey, Timestamp};
1111
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, RANGE};
1212
#[cfg(not(target_arch = "wasm32"))]
1313
use reqwest::redirect::Policy;
14-
use reqwest::StatusCode;
14+
use reqwest::{Response, StatusCode};
1515

1616
use crate::bud01::{
1717
BlossomAuthorization, BlossomAuthorizationScope, BlossomAuthorizationVerb,
1818
BlossomBuilderExtension,
1919
};
2020
use crate::bud02::BlobDescriptor;
21+
use crate::error::Error;
2122

2223
/// A client for interacting with a Blossom server
2324
///
@@ -57,7 +58,7 @@ impl BlossomClient {
5758
content_type: Option<String>,
5859
authorization_options: Option<BlossomAuthorizationOptions>,
5960
signer: Option<&T>,
60-
) -> Result<BlobDescriptor, Box<dyn Error>>
61+
) -> Result<BlobDescriptor, Error>
6162
where
6263
T: NostrSigner,
6364
{
@@ -70,8 +71,9 @@ impl BlossomClient {
7071
let mut headers = HeaderMap::new();
7172

7273
if let Some(ct) = content_type {
73-
headers.insert(CONTENT_TYPE, ct.parse()?);
74+
headers.insert(CONTENT_TYPE, HeaderValue::from_str(&ct)?);
7475
}
76+
7577
if let Some(signer) = signer {
7678
let default_auth = self.default_auth(
7779
BlossomAuthorizationVerb::Upload,
@@ -87,14 +89,14 @@ impl BlossomClient {
8789

8890
request = request.headers(headers);
8991

90-
let response = request.send().await?;
92+
let response: Response = request.send().await?;
9193

9294
match response.status() {
9395
StatusCode::OK => {
9496
let descriptor: BlobDescriptor = response.json().await?;
9597
Ok(descriptor)
9698
}
97-
_ => Err(Self::extract_error("Failed to upload blob", &response)),
99+
_ => Err(Error::response("Failed to upload blob", response)),
98100
}
99101
}
100102

@@ -108,7 +110,7 @@ impl BlossomClient {
108110
until: Option<Timestamp>,
109111
authorization_options: Option<BlossomAuthorizationOptions>,
110112
signer: Option<&T>,
111-
) -> Result<Vec<BlobDescriptor>, Box<dyn Error>>
113+
) -> Result<Vec<BlobDescriptor>, Error>
112114
where
113115
T: NostrSigner,
114116
{
@@ -146,14 +148,14 @@ impl BlossomClient {
146148

147149
request = request.headers(headers);
148150

149-
let response = request.send().await?;
151+
let response: Response = request.send().await?;
150152

151153
match response.status() {
152154
StatusCode::OK => {
153155
let descriptors: Vec<BlobDescriptor> = response.json().await?;
154156
Ok(descriptors)
155157
}
156-
_ => Err(Self::extract_error("Failed to list blobs", &response)),
158+
_ => Err(Error::response("Failed to list blobs", response)),
157159
}
158160
}
159161

@@ -166,7 +168,7 @@ impl BlossomClient {
166168
range: Option<String>,
167169
authorization_options: Option<BlossomAuthorizationOptions>,
168170
signer: Option<&T>,
169-
) -> Result<Vec<u8>, Box<dyn Error>>
171+
) -> Result<Vec<u8>, Error>
170172
where
171173
T: NostrSigner,
172174
{
@@ -193,23 +195,23 @@ impl BlossomClient {
193195

194196
request = request.headers(headers);
195197

196-
let response = request.send().await?;
198+
let response: Response = request.send().await?;
197199

198200
if response.status().is_redirection() {
199201
match response.headers().get("Location") {
200202
Some(location) => {
201-
let location_str = location.to_str()?;
203+
let location_str: &str = location.to_str()?;
202204
if !location_str.contains(&sha256.to_string()) {
203-
return Err("Redirect URL does not contain sha256 hash".into());
205+
return Err(Error::RedirectUrlDoesNotContainSha256);
204206
}
205207
}
206-
None => return Err("Redirect response missing Location header".into()),
208+
None => return Err(Error::RedirectResponseMissingLocationHeader),
207209
}
208210
}
209211

210212
match response.status() {
211213
StatusCode::OK | StatusCode::PARTIAL_CONTENT => Ok(response.bytes().await?.to_vec()),
212-
_ => Err(Self::extract_error("Failed to get blob", &response)),
214+
_ => Err(Error::response("Failed to get blob", response)),
213215
}
214216
}
215217

@@ -221,7 +223,7 @@ impl BlossomClient {
221223
sha256: Sha256Hash,
222224
authorization_options: Option<BlossomAuthorizationOptions>,
223225
signer: Option<&T>,
224-
) -> Result<bool, Box<dyn Error>>
226+
) -> Result<bool, Error>
225227
where
226228
T: NostrSigner,
227229
{
@@ -247,15 +249,12 @@ impl BlossomClient {
247249
request = request.headers(headers);
248250
}
249251

250-
let response = request.send().await?;
252+
let response: Response = request.send().await?;
251253

252254
match response.status() {
253-
reqwest::StatusCode::OK => Ok(true),
254-
reqwest::StatusCode::NOT_FOUND => Ok(false),
255-
_ => Err(Self::extract_error(
256-
"Unexpected HTTP status code",
257-
&response,
258-
)),
255+
StatusCode::OK => Ok(true),
256+
StatusCode::NOT_FOUND => Ok(false),
257+
_ => Err(Error::response("Unexpected HTTP status code", response)),
259258
}
260259
}
261260

@@ -267,7 +266,7 @@ impl BlossomClient {
267266
sha256: Sha256Hash,
268267
authorization_options: Option<BlossomAuthorizationOptions>,
269268
signer: &T,
270-
) -> Result<(), Box<dyn Error>>
269+
) -> Result<(), Error>
271270
where
272271
T: NostrSigner,
273272
{
@@ -287,12 +286,12 @@ impl BlossomClient {
287286
let auth_header = Self::build_auth_header(signer, &final_auth).await?;
288287
headers.insert(AUTHORIZATION, auth_header);
289288

290-
let response = self.client.delete(&url).headers(headers).send().await?;
289+
let response: Response = self.client.delete(&url).headers(headers).send().await?;
291290

292291
if response.status().is_success() {
293292
Ok(())
294293
} else {
295-
Err(Self::extract_error("Failed to delete blob", &response))
294+
Err(Error::response("Failed to delete blob", response))
296295
}
297296
}
298297

@@ -334,29 +333,17 @@ impl BlossomClient {
334333
async fn build_auth_header<T>(
335334
signer: &T,
336335
authz: &BlossomAuthorization,
337-
) -> Result<HeaderValue, Box<dyn Error>>
336+
) -> Result<HeaderValue, Error>
338337
where
339338
T: NostrSigner,
340339
{
341-
let pubkey = signer.get_public_key().await?;
342-
let auth_event = EventBuilder::blossom_auth(authz.clone())
343-
.build(pubkey)
340+
let auth_event: Event = EventBuilder::blossom_auth(authz.clone())
344341
.sign(signer)
345342
.await?;
346-
let auth_bytes = serde_json::to_vec(&auth_event)?;
347-
let encoded_auth = base64::engine::general_purpose::STANDARD.encode(auth_bytes);
348-
HeaderValue::from_str(&format!("Nostr {}", encoded_auth)).map_err(From::from)
349-
}
350-
351-
/// Helper function to extract error message from a response.
352-
fn extract_error(prefix: &str, response: &reqwest::Response) -> Box<dyn Error> {
353-
let reason = response
354-
.headers()
355-
.get("X-Reason")
356-
.map(|h| h.to_str().unwrap_or("Unknown reason").to_string())
357-
.unwrap_or_else(|| "No reason provided".to_string());
358-
let message = format!("{}: {} - {}", prefix, response.status(), reason);
359-
message.into()
343+
// TODO: use directly event.as_json
344+
let auth_bytes = serde_json::to_vec(&auth_event).unwrap();
345+
let encoded_auth = general_purpose::STANDARD.encode(auth_bytes);
346+
Ok(HeaderValue::from_str(&format!("Nostr {}", encoded_auth))?)
360347
}
361348
}
362349

@@ -366,7 +353,7 @@ pub struct BlossomAuthorizationOptions {
366353
/// A human readable string explaining to the user what the events intended use is
367354
pub content: Option<String>,
368355
/// A UNIX timestamp (in seconds) indicating when the authorization should be expired
369-
pub expiration: Option<nostr::Timestamp>,
356+
pub expiration: Option<Timestamp>,
370357
/// The type of action authorized by the user
371358
pub action: Option<BlossomAuthorizationVerb>,
372359
/// The scope of the authorization

crates/nostr-blossom/src/error.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) 2022-2023 Yuki Kishimoto
2+
// Copyright (c) 2023-2025 Rust Nostr Developers
3+
// Distributed under the MIT software license
4+
5+
//! Blossom error
6+
7+
use std::fmt;
8+
9+
use nostr::event::builder;
10+
use nostr::signer::SignerError;
11+
use reqwest::header::{InvalidHeaderValue, ToStrError};
12+
use reqwest::Response;
13+
14+
/// Blossom error
15+
#[derive(Debug)]
16+
pub enum Error {
17+
/// Nostr signer error
18+
Signer(SignerError),
19+
/// Event builder error
20+
EventBuilder(builder::Error),
21+
/// Reqwest error
22+
Reqwest(reqwest::Error),
23+
/// Invalid header value
24+
InvalidHeaderValue(InvalidHeaderValue),
25+
/// To string error
26+
ToStr(ToStrError),
27+
/// Response error
28+
Response {
29+
/// Prefix for the error message
30+
prefix: String,
31+
/// Response
32+
res: Response,
33+
},
34+
/// Returned when a redirect URL does not contain the expected hash
35+
RedirectUrlDoesNotContainSha256,
36+
/// Returned when a redirect response is missing the Location header
37+
RedirectResponseMissingLocationHeader,
38+
}
39+
40+
impl Error {
41+
#[inline]
42+
pub(super) fn response<S>(prefix: S, res: Response) -> Self
43+
where
44+
S: Into<String>,
45+
{
46+
Self::Response {
47+
prefix: prefix.into(),
48+
res,
49+
}
50+
}
51+
}
52+
53+
impl std::error::Error for Error {}
54+
55+
impl fmt::Display for Error {
56+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57+
match self {
58+
Self::Signer(e) => write!(f, "{e}"),
59+
Self::EventBuilder(e) => write!(f, "{e}"),
60+
Self::Reqwest(e) => write!(f, "{e}"),
61+
Self::InvalidHeaderValue(e) => write!(f, "{e}"),
62+
Self::ToStr(e) => write!(f, "{e}"),
63+
Self::Response { prefix, res } => {
64+
let reason: &str = res
65+
.headers()
66+
.get("X-Reason")
67+
.map(|h| h.to_str().unwrap_or("Unknown reason"))
68+
.unwrap_or_else(|| "No reason provided");
69+
write!(f, "{prefix}: {} - {reason}", res.status())
70+
}
71+
Self::RedirectUrlDoesNotContainSha256 => {
72+
write!(f, "Redirect URL does not contain SHA256")
73+
}
74+
Self::RedirectResponseMissingLocationHeader => {
75+
write!(f, "Redirect response missing 'Location' header")
76+
}
77+
}
78+
}
79+
}
80+
81+
impl From<SignerError> for Error {
82+
fn from(e: SignerError) -> Self {
83+
Self::Signer(e)
84+
}
85+
}
86+
87+
impl From<builder::Error> for Error {
88+
fn from(e: builder::Error) -> Self {
89+
Self::EventBuilder(e)
90+
}
91+
}
92+
93+
impl From<reqwest::Error> for Error {
94+
fn from(e: reqwest::Error) -> Self {
95+
Self::Reqwest(e)
96+
}
97+
}
98+
99+
impl From<InvalidHeaderValue> for Error {
100+
fn from(e: InvalidHeaderValue) -> Self {
101+
Self::InvalidHeaderValue(e)
102+
}
103+
}
104+
105+
impl From<ToStrError> for Error {
106+
fn from(e: ToStrError) -> Self {
107+
Self::ToStr(e)
108+
}
109+
}

crates/nostr-blossom/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
pub mod bud01;
1818
pub mod bud02;
1919
pub mod client;
20+
pub mod error;
2021
pub mod prelude;

crates/nostr-blossom/src/prelude.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
pub use crate::bud01::*;
1313
pub use crate::bud02::*;
1414
pub use crate::client::*;
15+
pub use crate::error::*;

0 commit comments

Comments
 (0)