|
| 1 | +use async_trait::async_trait; |
| 2 | +use bcr_ebill_core::{NodeId, ServiceTraitBounds, bill::BillId, notification::BillEventType}; |
| 3 | +use borsh_derive::BorshSerialize; |
| 4 | +use nostr::hashes::Hash; |
| 5 | +use nostr::util::SECP256K1; |
| 6 | +use nostr::{hashes::sha256, nips::nip19::ToBech32}; |
| 7 | +use secp256k1::{Keypair, Message}; |
| 8 | +use serde::{Deserialize, Serialize}; |
| 9 | +use thiserror::Error; |
| 10 | + |
| 11 | +/// Generic result type |
| 12 | +pub type Result<T> = std::result::Result<T, super::Error>; |
| 13 | + |
| 14 | +/// Generic error type |
| 15 | +#[derive(Debug, Error)] |
| 16 | +pub enum Error { |
| 17 | + /// all errors originating from interacting with the web api |
| 18 | + #[error("External Email Web API error: {0}")] |
| 19 | + Api(#[from] reqwest::Error), |
| 20 | + /// all errors originating from invalid urls |
| 21 | + #[error("External Email Invalid Relay Url Error")] |
| 22 | + InvalidRelayUrl, |
| 23 | + /// all hex errors |
| 24 | + #[error("External Email Hex Error: {0}")] |
| 25 | + Hex(#[from] hex::FromHexError), |
| 26 | + /// all signature errors |
| 27 | + #[error("External Email Signature Error: {0}")] |
| 28 | + Signature(#[from] secp256k1::Error), |
| 29 | + /// all nostr key errors |
| 30 | + #[error("External Email Nostr Key Error")] |
| 31 | + NostrKey, |
| 32 | + /// all borsh errors |
| 33 | + #[error("External Email Borsh Error")] |
| 34 | + Borsh(#[from] borsh::io::Error), |
| 35 | +} |
| 36 | + |
| 37 | +#[cfg(test)] |
| 38 | +use mockall::automock; |
| 39 | + |
| 40 | +use crate::{external::file_storage::to_url, get_config}; |
| 41 | + |
| 42 | +#[cfg_attr(test, automock)] |
| 43 | +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] |
| 44 | +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] |
| 45 | +pub trait EmailClientApi: ServiceTraitBounds { |
| 46 | + /// Start register flow, receiving a challenge string |
| 47 | + async fn start(&self, relay_url: &str, npub: &str) -> Result<StartEmailRegisterResponse>; |
| 48 | + /// Register for email notifications |
| 49 | + async fn register( |
| 50 | + &self, |
| 51 | + relay_url: &str, |
| 52 | + email: &str, |
| 53 | + private_key: &nostr::SecretKey, |
| 54 | + challenge: &str, |
| 55 | + ) -> Result<RegisterEmailNotificationResponse>; |
| 56 | + /// Send a bill notification email |
| 57 | + async fn send_bill_notification( |
| 58 | + &self, |
| 59 | + relay_url: &str, |
| 60 | + kind: BillEventType, |
| 61 | + id: &BillId, |
| 62 | + receiver: &NodeId, |
| 63 | + private_key: &nostr::SecretKey, |
| 64 | + ) -> Result<()>; |
| 65 | +} |
| 66 | + |
| 67 | +#[derive(Debug, Clone, Default)] |
| 68 | +pub struct EmailClient { |
| 69 | + cl: reqwest::Client, |
| 70 | +} |
| 71 | + |
| 72 | +impl ServiceTraitBounds for EmailClient {} |
| 73 | + |
| 74 | +#[cfg(test)] |
| 75 | +impl ServiceTraitBounds for MockEmailClientApi {} |
| 76 | + |
| 77 | +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] |
| 78 | +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] |
| 79 | +impl EmailClientApi for EmailClient { |
| 80 | + async fn start(&self, relay_url: &str, npub: &str) -> Result<StartEmailRegisterResponse> { |
| 81 | + let req = StartEmailRegisterRequest { |
| 82 | + npub: npub.to_owned(), |
| 83 | + }; |
| 84 | + |
| 85 | + let resp: StartEmailRegisterResponse = self |
| 86 | + .cl |
| 87 | + .post(to_url(relay_url, "notifications/v1/start")?) |
| 88 | + .json(&req) |
| 89 | + .send() |
| 90 | + .await? |
| 91 | + .json() |
| 92 | + .await?; |
| 93 | + |
| 94 | + Ok(resp) |
| 95 | + } |
| 96 | + |
| 97 | + async fn register( |
| 98 | + &self, |
| 99 | + relay_url: &str, |
| 100 | + email: &str, |
| 101 | + private_key: &nostr::SecretKey, |
| 102 | + challenge: &str, |
| 103 | + ) -> Result<RegisterEmailNotificationResponse> { |
| 104 | + let key_pair = Keypair::from_secret_key(SECP256K1, private_key); |
| 105 | + let msg = Message::from_digest_slice(&hex::decode(challenge).map_err(Error::Hex)?) |
| 106 | + .map_err(Error::Signature)?; |
| 107 | + let signed_challenge = SECP256K1.sign_schnorr(&msg, &key_pair).to_string(); |
| 108 | + |
| 109 | + let npub = nostr::Keys::new(private_key.clone()) |
| 110 | + .public_key() |
| 111 | + .to_bech32() |
| 112 | + .map_err(|_| Error::NostrKey)?; |
| 113 | + |
| 114 | + let req = RegisterEmailNotificationRequest { |
| 115 | + email: email.to_owned(), |
| 116 | + ebill_url: get_config().app_url.to_owned(), |
| 117 | + npub, |
| 118 | + signed_challenge, |
| 119 | + }; |
| 120 | + |
| 121 | + let resp: RegisterEmailNotificationResponse = self |
| 122 | + .cl |
| 123 | + .post(to_url(relay_url, "notifications/v1/register")?) |
| 124 | + .json(&req) |
| 125 | + .send() |
| 126 | + .await? |
| 127 | + .json() |
| 128 | + .await?; |
| 129 | + |
| 130 | + Ok(resp) |
| 131 | + } |
| 132 | + |
| 133 | + async fn send_bill_notification( |
| 134 | + &self, |
| 135 | + relay_url: &str, |
| 136 | + kind: BillEventType, |
| 137 | + id: &BillId, |
| 138 | + receiver: &NodeId, |
| 139 | + private_key: &nostr::SecretKey, |
| 140 | + ) -> Result<()> { |
| 141 | + let sender_npub = nostr::Keys::new(private_key.clone()) |
| 142 | + .public_key() |
| 143 | + .to_bech32() |
| 144 | + .map_err(|_| Error::NostrKey)?; |
| 145 | + |
| 146 | + let payload = SendEmailNotificationPayload { |
| 147 | + kind: kind.to_string(), |
| 148 | + id: id.to_string(), |
| 149 | + sender: sender_npub, |
| 150 | + receiver: receiver.npub().to_bech32().map_err(|_| Error::NostrKey)?, |
| 151 | + }; |
| 152 | + |
| 153 | + let key_pair = Keypair::from_secret_key(SECP256K1, private_key); |
| 154 | + let serialized = borsh::to_vec(&payload).map_err(Error::Borsh)?; |
| 155 | + let hash: sha256::Hash = sha256::Hash::hash(&serialized); |
| 156 | + let msg = Message::from_digest(*hash.as_ref()); |
| 157 | + |
| 158 | + let signature = SECP256K1.sign_schnorr(&msg, &key_pair).to_string(); |
| 159 | + |
| 160 | + let req = SendEmailNotificationRequest { payload, signature }; |
| 161 | + |
| 162 | + self.cl |
| 163 | + .post(to_url(relay_url, "notifications/v1/send")?) |
| 164 | + .json(&req) |
| 165 | + .send() |
| 166 | + .await? |
| 167 | + .error_for_status()?; |
| 168 | + |
| 169 | + Ok(()) |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +#[derive(Debug, Serialize)] |
| 174 | +pub struct StartEmailRegisterRequest { |
| 175 | + pub npub: String, |
| 176 | +} |
| 177 | + |
| 178 | +#[derive(Debug, Deserialize)] |
| 179 | +pub struct StartEmailRegisterResponse { |
| 180 | + pub challenge: String, |
| 181 | + pub ttl_seconds: u32, |
| 182 | +} |
| 183 | + |
| 184 | +#[derive(Debug, Serialize)] |
| 185 | +pub struct RegisterEmailNotificationRequest { |
| 186 | + pub email: String, |
| 187 | + pub ebill_url: url::Url, |
| 188 | + pub npub: String, |
| 189 | + pub signed_challenge: String, |
| 190 | +} |
| 191 | + |
| 192 | +#[derive(Debug, Deserialize)] |
| 193 | +pub struct RegisterEmailNotificationResponse { |
| 194 | + pub preferences_token: String, |
| 195 | +} |
| 196 | + |
| 197 | +#[derive(Debug, Serialize)] |
| 198 | +pub struct SendEmailNotificationRequest { |
| 199 | + pub payload: SendEmailNotificationPayload, |
| 200 | + pub signature: String, |
| 201 | +} |
| 202 | + |
| 203 | +#[derive(Debug, Serialize, BorshSerialize)] |
| 204 | +pub struct SendEmailNotificationPayload { |
| 205 | + pub kind: String, |
| 206 | + pub id: String, |
| 207 | + pub sender: String, |
| 208 | + pub receiver: String, |
| 209 | +} |
0 commit comments