diff --git a/Cargo.lock b/Cargo.lock index 9a1a560ba..471704d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2158,6 +2158,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniscript" version = "9.0.2" @@ -2294,6 +2304,7 @@ version = "0.5.7" dependencies = [ "anyhow", "async-trait", + "base64 0.13.1", "bip39", "bitcoin 0.29.2", "console_error_panic_hook", @@ -2896,6 +2907,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -3785,6 +3797,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 5462d2dd3..5ad54f449 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -35,7 +35,7 @@ lightning-transaction-sync = { version = "0.0.118", features = ["esplora-async-h lightning-liquidity = { git = "https://github.com/johncantrell97/ldk-lsp-client.git", rev = "9e01757d20c04aa31c28de8c4ffab5442d547edc" } chrono = "0.4.22" futures-util = { version = "0.3", default-features = false } -reqwest = { version = "0.11", default-features = false, features = ["json"] } +reqwest = { version = "0.11", default-features = false, features = ["multipart", "json"] } async-trait = "0.1.68" url = { version = "2.3.1", features = ["serde"] } nostr = { version = "0.27.0", default-features = false, features = ["nip04", "nip05", "nip47", "nip57"] } diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 2a9e2e36b..5a956a17a 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -87,6 +87,7 @@ use lightning::{log_error, log_info, log_warn}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::{lnurl::LnUrl, AsyncClient as LnUrlClient, LnUrlResponse, Response}; use nostr_sdk::{Client, RelayPoolNotification}; +use reqwest::multipart::{Form, Part}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; @@ -1384,6 +1385,42 @@ impl MutinyWallet { Ok(()) } + /// Uploads a profile pic to nostr.build and returns the uploaded file's URL + pub async fn upload_profile_pic(&self, image_bytes: Vec) -> Result { + let client = reqwest::Client::new(); + + let form = Form::new().part("fileToUpload", Part::bytes(image_bytes)); + let res: NostrBuildResult = client + .post("https://nostr.build/api/v2/upload/profile") + .multipart(form) + .send() + .await + .map_err(|_| MutinyError::NostrError)? + .json() + .await + .map_err(|_| MutinyError::NostrError)?; + + if res.status != "success" { + log_error!( + self.logger, + "Error uploading profile picture: {}", + res.message + ); + return Err(MutinyError::NostrError); + } + + // get url from response body + if let Some(value) = res.data.first() { + return value + .get("url") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or(MutinyError::NostrError); + } + + Err(MutinyError::NostrError) + } + /// Makes a request to the primal api async fn primal_request( client: &reqwest::Client, @@ -2106,6 +2143,13 @@ pub(crate) async fn create_new_federation( }) } +#[derive(Deserialize)] +struct NostrBuildResult { + status: String, + message: String, + data: Vec, +} + #[cfg(test)] mod tests { use crate::{ diff --git a/mutiny-wasm/Cargo.toml b/mutiny-wasm/Cargo.toml index 27475714b..ba6182755 100644 --- a/mutiny-wasm/Cargo.toml +++ b/mutiny-wasm/Cargo.toml @@ -43,6 +43,7 @@ urlencoding = "2.1.2" once_cell = "1.18.0" payjoin = { version = "0.13.0", features = ["send", "base64"] } fedimint-core = "0.2.1" +base64 = "0.13.0" # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/mutiny-wasm/src/error.rs b/mutiny-wasm/src/error.rs index b05e715d9..4a34a4014 100644 --- a/mutiny-wasm/src/error.rs +++ b/mutiny-wasm/src/error.rs @@ -232,6 +232,12 @@ impl From for MutinyJsError { } } +impl From for MutinyJsError { + fn from(_e: base64::DecodeError) -> Self { + Self::InvalidArgumentsError + } +} + impl From for MutinyJsError { fn from(_e: bip39::Error) -> Self { Self::InvalidMnemonic diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index c6088e08f..1b17ea0b5 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -1674,6 +1674,12 @@ impl MutinyWallet { Ok(event_id.to_hex()) } + /// Uploads a profile pic to nostr.build and returns the uploaded file's URL + pub async fn upload_profile_pic(&self, img_base64: String) -> Result { + let bytes = base64::decode(&img_base64)?; + Ok(self.inner.upload_profile_pic(bytes).await?) + } + /// Resets the scorer and network graph. This can be useful if you get stuck in a bad state. #[wasm_bindgen] pub async fn reset_router(&self) -> Result<(), MutinyJsError> {