use slotmap::Key;
use tokio_with_wasm::alias as tokio;
#[derive(Debug, serde::Serialize)]
struct Request<'a> {
id: String,
method: &'a str,
params: Vec<String>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct Response {
pub id: String,
pub result: Option<String>,
pub error: Option<String>,
}
pub struct AuthURLHandler(Box<dyn Fn(&str) + Send + Sync>);
impl std::fmt::Debug for AuthURLHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "fn")
}
}
#[derive(thiserror::Error, Debug)]
pub enum RPCError {
#[error("we're not connected to any relay")]
NoRelays(Vec<String>),
#[error("response channel closed")]
NoResponse,
#[error("request timed out")]
Timeout,
#[error("request encryption failed: {0}")]
Encryption(#[from] crate::message_encryption::EncryptError),
#[error("response decryption failed: {0}")]
Decryption(#[from] crate::message_encryption::DecryptError),
#[error("bunker replied with an error: {0}")]
Response(String),
}
#[derive(thiserror::Error, Debug)]
pub enum ConnectError {
#[error("rpc call failed: {0}")]
RPC(#[from] RPCError),
#[error("bunker uri is invalid")]
URI(#[from] url::ParseError),
#[error("bunker uri contains an invalid public key")]
InvalidPublicKeyHost(#[from] crate::keys::PubKeyError),
}
#[derive(thiserror::Error, Debug)]
pub enum GetPublicKeyError {
#[error("rpc call failed: {0}")]
RPC(#[from] RPCError),
#[error("got an invalid public key")]
InvalidPublicKey(#[from] crate::keys::PubKeyError),
}
#[derive(thiserror::Error, Debug)]
pub enum FinalizeError {
#[error("bunker gave us an invalid event")]
InvalidEvent,
#[error("bunker gave us an event with an invalid signature")]
InvalidSignature,
#[error("rpc call failed: {0}")]
RPC(#[from] RPCError),
}
slotmap::new_key_type! { struct RequestKey ; }
#[derive(Clone, Debug)]
pub struct BunkerClient {
client_secret_key: crate::SecretKey,
pool: crate::Pool,
target: crate::PubKey,
relays: Vec<String>,
conversation_key: [u8; 32],
awaiting_responses: std::sync::Arc<
tokio::sync::Mutex<slotmap::SlotMap<RequestKey, tokio::sync::oneshot::Sender<Response>>>,
>,
on_auth_url: std::sync::Arc<Option<AuthURLHandler>>,
get_pubkey_response: std::sync::Arc<tokio::sync::Mutex<Option<crate::PubKey>>>,
}
impl BunkerClient {
pub fn new(
client_secret_key: crate::SecretKey,
target_pubkey: crate::PubKey,
relays: Vec<String>,
pool: crate::Pool,
on_auth_url: Option<AuthURLHandler>,
) -> Self {
let client_pubkey = client_secret_key.pubkey();
let conversation_key = crate::message_encryption::generate_conversation_key(
&target_pubkey,
&client_secret_key,
);
let bunker = Self {
client_secret_key,
pool,
target: target_pubkey,
relays,
conversation_key,
awaiting_responses: std::sync::Arc::new(tokio::sync::Mutex::new(
slotmap::SlotMap::with_capacity_and_key(10),
)),
on_auth_url: std::sync::Arc::new(on_auth_url),
get_pubkey_response: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
};
let pool = bunker.pool.clone();
let relays = bunker.relays.clone();
let on_auth_url = bunker.on_auth_url.clone();
let awaiting_responses = bunker.awaiting_responses.clone();
tokio::spawn(async move {
let filter = crate::Filter {
kinds: Some(vec![crate::Kind(24133)]),
tags: Some(vec![crate::filter::TagQuery(
"#p".to_string(),
vec![client_pubkey.to_hex()],
)]),
since: Some(crate::Timestamp::now()),
..Default::default()
};
for url in relays.iter() {
let url = url.clone();
let filter = filter.clone();
let pool = pool.clone();
let on_auth_url = on_auth_url.clone();
let awaiting_responses = awaiting_responses.clone();
tokio::spawn(async move {
if let Ok(relay) = pool.ensure_relay(&url).await {
let mut sub = relay.subscribe(filter, Default::default()).await;
while let Some(occ) = sub.recv().await {
if let crate::relay::Occurrence::Event(event) = occ {
if event.kind.0 != 24133 {
continue;
}
if let Ok(plain) = crate::message_encryption::decrypt(
&event.content,
&conversation_key,
)
&& let Ok(resp) = serde_json::from_str::<Response>(&plain) {
let rk = match lowercase_hex::decode_to_array::<&str, 8>(
&resp.id,
) {
Ok(bytes) => RequestKey(slotmap::KeyData::from_ffi(
u64::from_be_bytes(bytes),
)),
Err(_) => continue,
};
if resp.result.as_deref() == Some("auth_url")
&& let Some(auth_url) = &resp.error {
{
if let Some(on_auth_fn) = on_auth_url.as_ref() {
on_auth_fn.0(auth_url);
}
}
}
if let Some(dispatcher) =
awaiting_responses.lock().await.remove(rk)
{
let _ = dispatcher.send(resp);
}
}
}
}
}
});
}
});
bunker
}
pub async fn connect(
client_secret_key: crate::SecretKey,
bunker_url: &str,
pool: crate::Pool,
on_auth_url: Option<AuthURLHandler>,
) -> Result<Self, ConnectError> {
let url = url::Url::parse(bunker_url)?;
let host = url
.host_str()
.ok_or(ConnectError::URI(url::ParseError::EmptyHost))?;
let pk: crate::PubKey = host.parse()?;
let relays = url
.query_pairs()
.filter(|(k, _)| k == "relay")
.map(|(_, v)| v.to_string())
.collect();
let secret = url
.query_pairs()
.find(|(k, _)| k == "secret")
.map(|(_, v)| v.to_string());
let mut params = vec![pk.to_hex()];
if let Some(secret) = secret {
params.push(secret);
}
let bunker = Self::new(client_secret_key, pk, relays, pool, on_auth_url);
bunker.rpc("connect", params).await?;
Ok(bunker)
}
pub async fn ping(&self) -> Result<(), RPCError> {
self.rpc("ping", vec![]).await?;
Ok(())
}
pub async fn get_public_key(&self) -> Result<crate::PubKey, GetPublicKeyError> {
let mut guard = self.get_pubkey_response.lock().await;
if let Some(pk) = guard.as_ref() {
return Ok(pk.to_owned());
}
let resp = self.rpc("get_public_key", vec![]).await?;
let pk: crate::PubKey = resp.parse()?;
*guard = Some(pk);
Ok(pk)
}
pub async fn finalize_event(
&self,
event_template: crate::EventTemplate,
) -> Result<crate::Event, FinalizeError> {
let event_json = serde_json::json!(event_template).to_string();
let resp = self.rpc("sign_event", vec![event_json]).await?;
let event: crate::Event =
serde_json::from_str(&resp).map_err(|_| FinalizeError::InvalidEvent)?;
if !event.verify_signature() {
return Err(FinalizeError::InvalidSignature);
}
Ok(event)
}
pub async fn nip44_encrypt(
&self,
target_pubkey: &crate::PubKey,
plaintext: &str,
) -> Result<String, RPCError> {
self.rpc(
"nip44_encrypt",
vec![target_pubkey.to_hex(), plaintext.to_string()],
)
.await
}
pub async fn nip44_decrypt(
&self,
target_pubkey: &crate::PubKey,
ciphertext: &str,
) -> Result<String, RPCError> {
self.rpc(
"nip44_decrypt",
vec![target_pubkey.to_hex(), ciphertext.to_string()],
)
.await
}
pub async fn nip04_encrypt(
&self,
target_pubkey: &crate::PubKey,
plaintext: &str,
) -> Result<String, RPCError> {
self.rpc(
"nip04_encrypt",
vec![target_pubkey.to_hex(), plaintext.to_string()],
)
.await
}
pub async fn nip04_decrypt(
&self,
target_pubkey: &crate::PubKey,
ciphertext: &str,
) -> Result<String, RPCError> {
self.rpc(
"nip04_decrypt",
vec![target_pubkey.to_hex(), ciphertext.to_string()],
)
.await
}
async fn rpc(&self, method: &str, params: Vec<String>) -> Result<String, RPCError> {
let (tx, rx) = tokio::sync::oneshot::channel::<Response>();
let rk = self.awaiting_responses.lock().await.insert(tx);
let req = Request {
id: lowercase_hex::encode(rk.data().as_ffi().to_le_bytes()),
method,
params,
};
let req_json =
serde_json::to_string(&req).expect("request should not fail to encode as json");
let content = crate::message_encryption::encrypt(&req_json, &self.conversation_key, None)?;
let event = crate::EventTemplate {
content,
created_at: crate::Timestamp::now(),
kind: crate::Kind(24133),
tags: crate::Tags(vec![vec!["p".to_string(), self.target.to_hex()]]),
}
.finalize(&self.client_secret_key);
let mut sent = false;
for url in self.relays.iter() {
if let Some(relay) = self.pool.get_relay(url).await
&& relay.publish(event.clone()).await.is_ok() {
sent = true;
}
}
if !sent {
return Err(RPCError::NoRelays(self.relays.clone()));
}
match tokio::time::timeout(std::time::Duration::from_secs(15), rx).await {
Ok(Ok(resp)) => {
if let Some(err) = resp.error {
Err(RPCError::Response(err))
} else {
Ok(resp.result.unwrap_or_default())
}
}
Ok(Err(_)) => {
self.awaiting_responses.lock().await.remove(rk);
Err(RPCError::NoResponse)
}
Err(_) => {
self.awaiting_responses.lock().await.remove(rk);
Err(RPCError::Timeout)
}
}
}
}