split notedeck into crates
This splits notedeck into crates, separating the browser chrome and individual apps: * notedeck: binary file, browser chrome * notedeck_columns: our columns app * enostr: same as before We still need to do more work to cleanly separate the chrome apis from the app apis. Soon I will create notedeck-notebook to see what makes sense to be shared between the apps. Some obvious ones that come to mind: 1. ImageCache We will likely want to move this to the notedeck crate, as most apps will want some kind of image cache. In web browsers, web pages do not need to worry about this, so we will likely have to do something similar 2. Ndb Since NdbRef is threadsafe and Ndb is an Arc<NdbRef>, it can be safely copied to each app. This will simplify things. In the future we might want to create an abstraction over this? Maybe each app shouldn't have access to the same database... we assume the data in DBs are all public anyways, but if we have unwrapped giftwraps that could be a problem. 3. RelayPool / Subscription Manager The browser should probably maintain these. Then apps can use ken's high level subscription manager api and not have to worry about connection pool details 4. Accounts Accounts and key management should be handled by the chrome. Apps should only have a simple signer interface. That's all for now, just something to think about! Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
59
crates/enostr/src/client/message.rs
Normal file
59
crates/enostr/src/client/message.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::{Error, Note};
|
||||
use nostrdb::Filter;
|
||||
use serde_json::json;
|
||||
|
||||
/// Messages sent by clients, received by relays
|
||||
#[derive(Debug)]
|
||||
pub enum ClientMessage {
|
||||
Event {
|
||||
note: Note,
|
||||
},
|
||||
Req {
|
||||
sub_id: String,
|
||||
filters: Vec<Filter>,
|
||||
},
|
||||
Close {
|
||||
sub_id: String,
|
||||
},
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl ClientMessage {
|
||||
pub fn event(note: Note) -> Self {
|
||||
ClientMessage::Event { note }
|
||||
}
|
||||
|
||||
pub fn raw(raw: String) -> Self {
|
||||
ClientMessage::Raw(raw)
|
||||
}
|
||||
|
||||
pub fn req(sub_id: String, filters: Vec<Filter>) -> Self {
|
||||
ClientMessage::Req { sub_id, filters }
|
||||
}
|
||||
|
||||
pub fn close(sub_id: String) -> Self {
|
||||
ClientMessage::Close { sub_id }
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String, Error> {
|
||||
Ok(match self {
|
||||
Self::Event { note } => json!(["EVENT", note]).to_string(),
|
||||
Self::Raw(raw) => raw.clone(),
|
||||
Self::Req { sub_id, filters } => {
|
||||
if filters.is_empty() {
|
||||
format!("[\"REQ\",\"{}\",{{ }}]", sub_id)
|
||||
} else if filters.len() == 1 {
|
||||
let filters_json_str = filters[0].json()?;
|
||||
format!("[\"REQ\",\"{}\",{}]", sub_id, filters_json_str)
|
||||
} else {
|
||||
let filters_json_str: Result<Vec<String>, Error> = filters
|
||||
.iter()
|
||||
.map(|f| f.json().map_err(Into::<Error>::into))
|
||||
.collect();
|
||||
format!("[\"REQ\",\"{}\",{}]", sub_id, filters_json_str?.join(","))
|
||||
}
|
||||
}
|
||||
Self::Close { sub_id } => json!(["CLOSE", sub_id]).to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
3
crates/enostr/src/client/mod.rs
Normal file
3
crates/enostr/src/client/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod message;
|
||||
|
||||
pub use message::ClientMessage;
|
||||
74
crates/enostr/src/error.rs
Normal file
74
crates/enostr/src/error.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//use nostr::prelude::secp256k1;
|
||||
use std::array::TryFromSliceError;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Empty,
|
||||
DecodeFailed,
|
||||
HexDecodeFailed,
|
||||
InvalidBech32,
|
||||
InvalidByteSize,
|
||||
InvalidSignature,
|
||||
InvalidPublicKey,
|
||||
// Secp(secp256k1::Error),
|
||||
Json(serde_json::Error),
|
||||
Nostrdb(nostrdb::Error),
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Empty => write!(f, "message is empty"),
|
||||
Self::DecodeFailed => write!(f, "decoding failed"),
|
||||
Self::InvalidSignature => write!(f, "invalid signature"),
|
||||
Self::HexDecodeFailed => write!(f, "hex decoding failed"),
|
||||
Self::InvalidByteSize => write!(f, "invalid byte size"),
|
||||
Self::InvalidBech32 => write!(f, "invalid bech32 string"),
|
||||
Self::InvalidPublicKey => write!(f, "invalid public key"),
|
||||
//Self::Secp(e) => write!(f, "{e}"),
|
||||
Self::Json(e) => write!(f, "{e}"),
|
||||
Self::Nostrdb(e) => write!(f, "{e}"),
|
||||
Self::Generic(e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TryFromSliceError> for Error {
|
||||
fn from(_e: TryFromSliceError) -> Self {
|
||||
Error::InvalidByteSize
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hex::FromHexError> for Error {
|
||||
fn from(_e: hex::FromHexError) -> Self {
|
||||
Error::HexDecodeFailed
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl From<secp256k1::Error> for Error {
|
||||
fn from(e: secp256k1::Error) -> Self {
|
||||
Error::Secp(e)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nostrdb::Error> for Error {
|
||||
fn from(e: nostrdb::Error) -> Self {
|
||||
Error::Nostrdb(e)
|
||||
}
|
||||
}
|
||||
1
crates/enostr/src/filter.rs
Normal file
1
crates/enostr/src/filter.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub type Filter = nostrdb::Filter;
|
||||
139
crates/enostr/src/keypair.rs
Normal file
139
crates/enostr/src/keypair.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use nostr::nips::nip49::EncryptedSecretKey;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::Pubkey;
|
||||
use crate::SecretKey;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct Keypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub secret_key: Option<SecretKey>,
|
||||
}
|
||||
|
||||
impl Keypair {
|
||||
pub fn from_secret(secret_key: SecretKey) -> Self {
|
||||
let cloned_secret_key = secret_key.clone();
|
||||
let nostr_keys = nostr::Keys::new(secret_key);
|
||||
Keypair {
|
||||
pubkey: Pubkey::new(nostr_keys.public_key().to_bytes()),
|
||||
secret_key: Some(cloned_secret_key),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(pubkey: Pubkey, secret_key: Option<SecretKey>) -> Self {
|
||||
Keypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn only_pubkey(pubkey: Pubkey) -> Self {
|
||||
Keypair {
|
||||
pubkey,
|
||||
secret_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_full(&self) -> Option<FilledKeypair<'_>> {
|
||||
self.secret_key.as_ref().map(|secret_key| FilledKeypair {
|
||||
pubkey: &self.pubkey,
|
||||
secret_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct FullKeypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub secret_key: SecretKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub struct FilledKeypair<'a> {
|
||||
pub pubkey: &'a Pubkey,
|
||||
pub secret_key: &'a SecretKey,
|
||||
}
|
||||
|
||||
impl<'a> FilledKeypair<'a> {
|
||||
pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self {
|
||||
FilledKeypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn to_full(&self) -> FullKeypair {
|
||||
FullKeypair {
|
||||
pubkey: self.pubkey.to_owned(),
|
||||
secret_key: self.secret_key.to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FullKeypair {
|
||||
pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self {
|
||||
FullKeypair { pubkey, secret_key }
|
||||
}
|
||||
|
||||
pub fn to_filled(&self) -> FilledKeypair<'_> {
|
||||
FilledKeypair::new(&self.pubkey, &self.secret_key)
|
||||
}
|
||||
|
||||
pub fn generate() -> Self {
|
||||
let mut rng = nostr::secp256k1::rand::rngs::OsRng;
|
||||
let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng);
|
||||
let (xopk, _) = secret_key.x_only_public_key(&nostr::SECP256K1);
|
||||
let secret_key = nostr::SecretKey::from(*secret_key);
|
||||
FullKeypair {
|
||||
pubkey: Pubkey::new(xopk.serialize()),
|
||||
secret_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_keypair(self) -> Keypair {
|
||||
Keypair {
|
||||
pubkey: self.pubkey,
|
||||
secret_key: Some(self.secret_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Keypair {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Keypair:\n\tpublic: {}\n\tsecret: {}",
|
||||
self.pubkey,
|
||||
match self.secret_key {
|
||||
Some(_) => "Some(<hidden>)",
|
||||
None => "None",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FullKeypair {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Keypair:\n\tpublic: {}\n\tsecret: <hidden>", self.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SerializableKeypair {
|
||||
pub pubkey: Pubkey,
|
||||
pub encrypted_secret_key: Option<EncryptedSecretKey>,
|
||||
}
|
||||
|
||||
impl SerializableKeypair {
|
||||
pub fn from_keypair(kp: &Keypair, pass: &str, log_n: u8) -> Self {
|
||||
Self {
|
||||
pubkey: kp.pubkey,
|
||||
encrypted_secret_key: kp.secret_key.clone().and_then(|s| {
|
||||
EncryptedSecretKey::new(&s, pass, log_n, nostr::nips::nip49::KeySecurity::Weak).ok()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_keypair(&self, pass: &str) -> Keypair {
|
||||
Keypair::new(
|
||||
self.pubkey,
|
||||
self.encrypted_secret_key
|
||||
.and_then(|e| e.to_secret_key(pass).ok()),
|
||||
)
|
||||
}
|
||||
}
|
||||
23
crates/enostr/src/lib.rs
Normal file
23
crates/enostr/src/lib.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
mod client;
|
||||
mod error;
|
||||
mod filter;
|
||||
mod keypair;
|
||||
mod note;
|
||||
mod profile;
|
||||
mod pubkey;
|
||||
mod relay;
|
||||
|
||||
pub use client::ClientMessage;
|
||||
pub use error::Error;
|
||||
pub use ewebsock;
|
||||
pub use filter::Filter;
|
||||
pub use keypair::{FilledKeypair, FullKeypair, Keypair, SerializableKeypair};
|
||||
pub use nostr::SecretKey;
|
||||
pub use note::{Note, NoteId};
|
||||
pub use profile::Profile;
|
||||
pub use pubkey::Pubkey;
|
||||
pub use relay::message::{RelayEvent, RelayMessage};
|
||||
pub use relay::pool::{PoolEvent, RelayPool};
|
||||
pub use relay::{Relay, RelayStatus};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||
135
crates/enostr/src/note.rs
Normal file
135
crates/enostr/src/note.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::{Error, Pubkey};
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct NoteId([u8; 32]);
|
||||
|
||||
impl fmt::Debug for NoteId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.hex())
|
||||
}
|
||||
}
|
||||
|
||||
static HRP_NOTE: nostr::bech32::Hrp = nostr::bech32::Hrp::parse_unchecked("note");
|
||||
|
||||
impl NoteId {
|
||||
pub fn new(bytes: [u8; 32]) -> Self {
|
||||
NoteId(bytes)
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn hex(&self) -> String {
|
||||
hex::encode(self.bytes())
|
||||
}
|
||||
|
||||
pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
|
||||
let evid = NoteId(hex::decode(hex_str)?.as_slice().try_into().unwrap());
|
||||
Ok(evid)
|
||||
}
|
||||
|
||||
pub fn to_bech(&self) -> Option<String> {
|
||||
nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NOTE, &self.0).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Event is the struct used to represent a Nostr event
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Note {
|
||||
/// 32-bytes sha256 of the the serialized event data
|
||||
pub id: NoteId,
|
||||
/// 32-bytes hex-encoded public key of the event creator
|
||||
pub pubkey: Pubkey,
|
||||
/// unix timestamp in seconds
|
||||
pub created_at: u64,
|
||||
/// integer
|
||||
/// 0: NostrEvent
|
||||
pub kind: u64,
|
||||
/// Tags
|
||||
pub tags: Vec<Vec<String>>,
|
||||
/// arbitrary string
|
||||
pub content: String,
|
||||
/// 64-bytes signature of the sha256 hash of the serialized event data, which is the same as the "id" field
|
||||
pub sig: String,
|
||||
}
|
||||
|
||||
// Implement Hash trait
|
||||
impl Hash for Note {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Note {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Note {}
|
||||
|
||||
impl Note {
|
||||
pub fn from_json(s: &str) -> Result<Self, Error> {
|
||||
serde_json::from_str(s).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<Self, Error> {
|
||||
Err(Error::InvalidSignature)
|
||||
}
|
||||
|
||||
/// This is just for serde sanity checking
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new_dummy(
|
||||
id: &str,
|
||||
pubkey: &str,
|
||||
created_at: u64,
|
||||
kind: u64,
|
||||
tags: Vec<Vec<String>>,
|
||||
content: &str,
|
||||
sig: &str,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Note {
|
||||
id: NoteId::from_hex(id)?,
|
||||
pubkey: Pubkey::from_hex(pubkey)?,
|
||||
created_at,
|
||||
kind,
|
||||
tags,
|
||||
content: content.to_string(),
|
||||
sig: sig.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Note {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
Note::from_json(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Custom serialize function for Pubkey
|
||||
impl Serialize for NoteId {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.hex())
|
||||
}
|
||||
}
|
||||
|
||||
// Custom deserialize function for Pubkey
|
||||
impl<'de> Deserialize<'de> for NoteId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
NoteId::from_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
38
crates/enostr/src/profile.rs
Normal file
38
crates/enostr/src/profile.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Profile(Value);
|
||||
|
||||
impl Profile {
|
||||
pub fn new(value: Value) -> Profile {
|
||||
Profile(value)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
self.0["name"].as_str()
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
self.0["display_name"].as_str()
|
||||
}
|
||||
|
||||
pub fn lud06(&self) -> Option<&str> {
|
||||
self.0["lud06"].as_str()
|
||||
}
|
||||
|
||||
pub fn lud16(&self) -> Option<&str> {
|
||||
self.0["lud16"].as_str()
|
||||
}
|
||||
|
||||
pub fn about(&self) -> Option<&str> {
|
||||
self.0["about"].as_str()
|
||||
}
|
||||
|
||||
pub fn picture(&self) -> Option<&str> {
|
||||
self.0["picture"].as_str()
|
||||
}
|
||||
|
||||
pub fn website(&self) -> Option<&str> {
|
||||
self.0["website"].as_str()
|
||||
}
|
||||
}
|
||||
125
crates/enostr/src/pubkey.rs
Normal file
125
crates/enostr/src/pubkey.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::Error;
|
||||
use nostr::bech32::Hrp;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)]
|
||||
pub struct Pubkey([u8; 32]);
|
||||
|
||||
static HRP_NPUB: Hrp = Hrp::parse_unchecked("npub");
|
||||
|
||||
impl Deref for Pubkey {
|
||||
type Target = [u8; 32];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Pubkey {
|
||||
pub fn new(data: [u8; 32]) -> Self {
|
||||
Self(data)
|
||||
}
|
||||
|
||||
pub fn hex(&self) -> String {
|
||||
hex::encode(self.bytes())
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Result<Self, Error> {
|
||||
match Pubkey::from_hex(s) {
|
||||
Ok(pk) => Ok(pk),
|
||||
Err(_) => Pubkey::try_from_bech32_string(s, false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_hex(hex_str: &str) -> Result<Self, Error> {
|
||||
Ok(Pubkey(hex::decode(hex_str)?.as_slice().try_into()?))
|
||||
}
|
||||
|
||||
pub fn try_from_hex_str_with_verify(hex_str: &str) -> Result<Self, Error> {
|
||||
let vec: Vec<u8> = hex::decode(hex_str)?;
|
||||
if vec.len() != 32 {
|
||||
Err(Error::HexDecodeFailed)
|
||||
} else {
|
||||
let _ = match nostr::secp256k1::XOnlyPublicKey::from_slice(&vec) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => Err(Error::InvalidPublicKey),
|
||||
}?;
|
||||
|
||||
Ok(Pubkey(vec.try_into().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_bech32_string(s: &str, verify: bool) -> Result<Self, Error> {
|
||||
let data = match nostr::bech32::decode(s) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(_) => Err(Error::InvalidBech32),
|
||||
}?;
|
||||
|
||||
if data.0 != HRP_NPUB {
|
||||
Err(Error::InvalidBech32)
|
||||
} else if data.1.len() != 32 {
|
||||
Err(Error::InvalidByteSize)
|
||||
} else {
|
||||
if verify {
|
||||
let _ = match nostr::secp256k1::XOnlyPublicKey::from_slice(&data.1) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(_) => Err(Error::InvalidPublicKey),
|
||||
}?;
|
||||
}
|
||||
Ok(Pubkey(data.1.try_into().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bech(&self) -> Option<String> {
|
||||
nostr::bech32::encode::<nostr::bech32::Bech32>(HRP_NPUB, &self.0).ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pubkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.hex())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Pubkey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.hex())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pubkey> for String {
|
||||
fn from(pk: Pubkey) -> Self {
|
||||
pk.hex()
|
||||
}
|
||||
}
|
||||
|
||||
// Custom serialize function for Pubkey
|
||||
impl Serialize for Pubkey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.hex())
|
||||
}
|
||||
}
|
||||
|
||||
// Custom deserialize function for Pubkey
|
||||
impl<'de> Deserialize<'de> for Pubkey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
debug!("decoding pubkey start");
|
||||
let s = String::deserialize(deserializer)?;
|
||||
debug!("decoding pubkey {}", &s);
|
||||
Pubkey::from_hex(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
281
crates/enostr/src/relay/message.rs
Normal file
281
crates/enostr/src/relay/message.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use crate::{Error, Result};
|
||||
use ewebsock::{WsEvent, WsMessage};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct CommandResult<'a> {
|
||||
event_id: &'a str,
|
||||
status: bool,
|
||||
message: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum RelayMessage<'a> {
|
||||
OK(CommandResult<'a>),
|
||||
Eose(&'a str),
|
||||
Event(&'a str, &'a str),
|
||||
Notice(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RelayEvent<'a> {
|
||||
Opened,
|
||||
Closed,
|
||||
Other(&'a WsMessage),
|
||||
Error(Error),
|
||||
Message(RelayMessage<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a WsEvent> for RelayEvent<'a> {
|
||||
fn from(event: &'a WsEvent) -> RelayEvent<'a> {
|
||||
match event {
|
||||
WsEvent::Opened => RelayEvent::Opened,
|
||||
WsEvent::Closed => RelayEvent::Closed,
|
||||
WsEvent::Message(ref ws_msg) => ws_msg.into(),
|
||||
WsEvent::Error(s) => RelayEvent::Error(Error::Generic(s.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a WsMessage> for RelayEvent<'a> {
|
||||
fn from(wsmsg: &'a WsMessage) -> RelayEvent<'a> {
|
||||
match wsmsg {
|
||||
WsMessage::Text(s) => match RelayMessage::from_json(s).map(RelayEvent::Message) {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => RelayEvent::Error(err),
|
||||
},
|
||||
wsmsg => RelayEvent::Other(wsmsg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RelayMessage<'a> {
|
||||
pub fn eose(subid: &'a str) -> Self {
|
||||
RelayMessage::Eose(subid)
|
||||
}
|
||||
|
||||
pub fn notice(msg: &'a str) -> Self {
|
||||
RelayMessage::Notice(msg)
|
||||
}
|
||||
|
||||
pub fn ok(event_id: &'a str, status: bool, message: &'a str) -> Self {
|
||||
RelayMessage::OK(CommandResult {
|
||||
event_id,
|
||||
status,
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn event(ev: &'a str, sub_id: &'a str) -> Self {
|
||||
RelayMessage::Event(sub_id, ev)
|
||||
}
|
||||
|
||||
pub fn from_json(msg: &'a str) -> Result<RelayMessage<'a>> {
|
||||
if msg.is_empty() {
|
||||
return Err(Error::Empty);
|
||||
}
|
||||
|
||||
// Notice
|
||||
// Relay response format: ["NOTICE", <message>]
|
||||
if &msg[0..=9] == "[\"NOTICE\"," {
|
||||
// TODO: there could be more than one space, whatever
|
||||
let start = if msg.as_bytes().get(10).copied() == Some(b' ') {
|
||||
12
|
||||
} else {
|
||||
11
|
||||
};
|
||||
let end = msg.len() - 2;
|
||||
return Ok(Self::notice(&msg[start..end]));
|
||||
}
|
||||
|
||||
// Event
|
||||
// Relay response format: ["EVENT", <subscription id>, <event JSON>]
|
||||
if &msg[0..=7] == "[\"EVENT\"" {
|
||||
let mut start = 9;
|
||||
while let Some(&b' ') = msg.as_bytes().get(start) {
|
||||
start += 1; // Move past optional spaces
|
||||
}
|
||||
if let Some(comma_index) = msg[start..].find(',') {
|
||||
let subid_end = start + comma_index;
|
||||
let subid = &msg[start..subid_end].trim().trim_matches('"');
|
||||
return Ok(Self::event(msg, subid));
|
||||
} else {
|
||||
return Ok(Self::event(msg, "fixme"));
|
||||
}
|
||||
}
|
||||
|
||||
// EOSE (NIP-15)
|
||||
// Relay response format: ["EOSE", <subscription_id>]
|
||||
if &msg[0..=7] == "[\"EOSE\"," {
|
||||
let start = if msg.as_bytes().get(8).copied() == Some(b' ') {
|
||||
10
|
||||
} else {
|
||||
9
|
||||
};
|
||||
let end = msg.len() - 2;
|
||||
return Ok(Self::eose(&msg[start..end]));
|
||||
}
|
||||
|
||||
// OK (NIP-20)
|
||||
// Relay response format: ["OK",<event_id>, <true|false>, <message>]
|
||||
if &msg[0..=5] == "[\"OK\"," {
|
||||
// TODO: fix this
|
||||
let event_id = &msg[7..71];
|
||||
let booly = &msg[73..77];
|
||||
let status: bool = if booly == "true" {
|
||||
true
|
||||
} else if booly == "false" {
|
||||
false
|
||||
} else {
|
||||
return Err(Error::DecodeFailed);
|
||||
};
|
||||
|
||||
return Ok(Self::ok(event_id, status, "fixme"));
|
||||
}
|
||||
|
||||
Err(Error::DecodeFailed)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_handle_valid_notice() -> Result<()> {
|
||||
let valid_notice_msg = r#"["NOTICE","Invalid event format!"]"#;
|
||||
let handled_valid_notice_msg = RelayMessage::notice("Invalid event format!".to_string());
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(valid_notice_msg)?,
|
||||
handled_valid_notice_msg
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_handle_invalid_notice() {
|
||||
//Missing content
|
||||
let invalid_notice_msg = r#"["NOTICE"]"#;
|
||||
//The content is not string
|
||||
let invalid_notice_msg_content = r#"["NOTICE": 404]"#;
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_notice_msg).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_notice_msg_content).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_valid_event() -> Result<()> {
|
||||
use tracing::debug;
|
||||
|
||||
let valid_event_msg = r#"["EVENT", "random_string", {"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe","created_at":1612809991,"kind":1,"tags":[],"content":"test","sig":"273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502"}]"#;
|
||||
|
||||
let id = "70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5";
|
||||
let pubkey = "379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe";
|
||||
let created_at = 1612809991;
|
||||
let kind = 1;
|
||||
let tags = vec![];
|
||||
let content = "test";
|
||||
let sig = "273a9cd5d11455590f4359500bccb7a89428262b96b3ea87a756b770964472f8c3e87f5d5e64d8d2e859a71462a3f477b554565c4f2f326cb01dd7620db71502";
|
||||
|
||||
let handled_event = Event::new_dummy(id, pubkey, created_at, kind, tags, content, sig);
|
||||
debug!("event {:?}", handled_event);
|
||||
|
||||
let msg = RelayMessage::from_json(valid_event_msg);
|
||||
debug!("msg {:?}", msg);
|
||||
|
||||
assert_eq!(
|
||||
msg?,
|
||||
RelayMessage::event(handled_event?, "random_string".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_invalid_event() {
|
||||
//Mising Event field
|
||||
let invalid_event_msg = r#"["EVENT","random_string"]"#;
|
||||
//Event JSON with incomplete content
|
||||
let invalid_event_msg_content = r#"["EVENT","random_string",{"id":"70b10f70c1318967eddf12527799411b1a9780ad9c43858f5e5fcd45486a13a5","pubkey":"379e863e8357163b5bce5d2688dc4f1dcc2d505222fb8d74db600f30535dfdfe"}]"#;
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_event_msg).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_event_msg_content).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_valid_eose() -> Result<()> {
|
||||
let valid_eose_msg = r#"["EOSE","random-subscription-id"]"#;
|
||||
let handled_valid_eose_msg = RelayMessage::eose("random-subscription-id".to_string());
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(valid_eose_msg)?,
|
||||
handled_valid_eose_msg
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_handle_invalid_eose() {
|
||||
// Missing subscription ID
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["EOSE"]"#).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
|
||||
// The subscription ID is not string
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["EOSE",404]"#).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_valid_ok() -> Result<()> {
|
||||
let valid_ok_msg = r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",true,"pow: difficulty 25>=24"]"#;
|
||||
let handled_valid_ok_msg = RelayMessage::ok(
|
||||
"b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30".to_string(),
|
||||
true,
|
||||
"pow: difficulty 25>=24".into(),
|
||||
);
|
||||
|
||||
assert_eq!(RelayMessage::from_json(valid_ok_msg)?, handled_valid_ok_msg);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_handle_invalid_ok() {
|
||||
// Missing params
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(
|
||||
r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30"]"#
|
||||
)
|
||||
.unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
|
||||
// Invalid status
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,""]"#).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
|
||||
// Invalid message
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["OK","b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30",hello,404]"#).unwrap_err(),
|
||||
Error::DecodeFailed
|
||||
);
|
||||
}
|
||||
}
|
||||
99
crates/enostr/src/relay/mod.rs
Normal file
99
crates/enostr/src/relay/mod.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use ewebsock::{WsMessage, WsReceiver, WsSender};
|
||||
|
||||
use crate::{ClientMessage, Result};
|
||||
use nostrdb::Filter;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub mod message;
|
||||
pub mod pool;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RelayStatus {
|
||||
Connected,
|
||||
Connecting,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
pub struct Relay {
|
||||
pub url: String,
|
||||
pub status: RelayStatus,
|
||||
pub sender: WsSender,
|
||||
pub receiver: WsReceiver,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Relay {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Relay")
|
||||
.field("url", &self.url)
|
||||
.field("status", &self.status)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Relay {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
// Hashes the Relay by hashing the URL
|
||||
self.url.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Relay {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.url == other.url
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Relay {}
|
||||
|
||||
impl Relay {
|
||||
pub fn new(url: String, wakeup: impl Fn() + Send + Sync + 'static) -> Result<Self> {
|
||||
let status = RelayStatus::Connecting;
|
||||
let (sender, receiver) = ewebsock::connect_with_wakeup(&url, wakeup)?;
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
sender,
|
||||
receiver,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn send(&mut self, msg: &ClientMessage) {
|
||||
let json = match msg.to_json() {
|
||||
Ok(json) => {
|
||||
debug!("sending {} to {}", json, self.url);
|
||||
json
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error serializing json for filter: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let txt = WsMessage::Text(json);
|
||||
self.sender.send(txt);
|
||||
}
|
||||
|
||||
pub fn connect(&mut self, wakeup: impl Fn() + Send + Sync + 'static) -> Result<()> {
|
||||
let (sender, receiver) = ewebsock::connect_with_wakeup(&self.url, wakeup)?;
|
||||
self.status = RelayStatus::Connecting;
|
||||
self.sender = sender;
|
||||
self.receiver = receiver;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ping(&mut self) {
|
||||
let msg = WsMessage::Ping(vec![]);
|
||||
self.sender.send(msg);
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, subid: String, filters: Vec<Filter>) {
|
||||
info!(
|
||||
"sending '{}' subscription to relay pool: {:?}",
|
||||
subid, filters
|
||||
);
|
||||
self.send(&ClientMessage::req(subid, filters));
|
||||
}
|
||||
}
|
||||
254
crates/enostr/src/relay/pool.rs
Normal file
254
crates/enostr/src/relay/pool.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use crate::relay::{Relay, RelayStatus};
|
||||
use crate::{ClientMessage, Result};
|
||||
use nostrdb::Filter;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use ewebsock::{WsEvent, WsMessage};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolEvent<'a> {
|
||||
pub relay: &'a str,
|
||||
pub event: ewebsock::WsEvent,
|
||||
}
|
||||
|
||||
impl PoolEvent<'_> {
|
||||
pub fn into_owned(self) -> PoolEventBuf {
|
||||
PoolEventBuf {
|
||||
relay: self.relay.to_owned(),
|
||||
event: self.event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PoolEventBuf {
|
||||
pub relay: String,
|
||||
pub event: ewebsock::WsEvent,
|
||||
}
|
||||
|
||||
pub struct PoolRelay {
|
||||
pub relay: Relay,
|
||||
pub last_ping: Instant,
|
||||
pub last_connect_attempt: Instant,
|
||||
pub retry_connect_after: Duration,
|
||||
}
|
||||
|
||||
impl PoolRelay {
|
||||
pub fn new(relay: Relay) -> PoolRelay {
|
||||
PoolRelay {
|
||||
relay,
|
||||
last_ping: Instant::now(),
|
||||
last_connect_attempt: Instant::now(),
|
||||
retry_connect_after: Self::initial_reconnect_duration(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initial_reconnect_duration() -> Duration {
|
||||
Duration::from_secs(5)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RelayPool {
|
||||
pub relays: Vec<PoolRelay>,
|
||||
pub ping_rate: Duration,
|
||||
}
|
||||
|
||||
impl Default for RelayPool {
|
||||
fn default() -> Self {
|
||||
RelayPool::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RelayPool {
|
||||
// Constructs a new, empty RelayPool.
|
||||
pub fn new() -> RelayPool {
|
||||
RelayPool {
|
||||
relays: vec![],
|
||||
ping_rate: Duration::from_secs(25),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ping_rate(&mut self, duration: Duration) -> &mut Self {
|
||||
self.ping_rate = duration;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has(&self, url: &str) -> bool {
|
||||
for relay in &self.relays {
|
||||
if relay.relay.url == url {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn urls(&self) -> BTreeSet<String> {
|
||||
self.relays
|
||||
.iter()
|
||||
.map(|pool_relay| pool_relay.relay.url.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn send(&mut self, cmd: &ClientMessage) {
|
||||
for relay in &mut self.relays {
|
||||
relay.relay.send(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsubscribe(&mut self, subid: String) {
|
||||
for relay in &mut self.relays {
|
||||
relay.relay.send(&ClientMessage::close(subid.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&mut self, subid: String, filter: Vec<Filter>) {
|
||||
for relay in &mut self.relays {
|
||||
relay.relay.subscribe(subid.clone(), filter.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep relay connectiongs alive by pinging relays that haven't been
|
||||
/// pinged in awhile. Adjust ping rate with [`ping_rate`].
|
||||
pub fn keepalive_ping(&mut self, wakeup: impl Fn() + Send + Sync + Clone + 'static) {
|
||||
for relay in &mut self.relays {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
match relay.relay.status {
|
||||
RelayStatus::Disconnected => {
|
||||
let reconnect_at = relay.last_connect_attempt + relay.retry_connect_after;
|
||||
if now > reconnect_at {
|
||||
relay.last_connect_attempt = now;
|
||||
let next_duration = Duration::from_millis(
|
||||
((relay.retry_connect_after.as_millis() as f64) * 1.5) as u64,
|
||||
);
|
||||
debug!(
|
||||
"bumping reconnect duration from {:?} to {:?} and retrying connect",
|
||||
relay.retry_connect_after, next_duration
|
||||
);
|
||||
relay.retry_connect_after = next_duration;
|
||||
if let Err(err) = relay.relay.connect(wakeup.clone()) {
|
||||
error!("error connecting to relay: {}", err);
|
||||
}
|
||||
} else {
|
||||
// let's wait a bit before we try again
|
||||
}
|
||||
}
|
||||
|
||||
RelayStatus::Connected => {
|
||||
relay.retry_connect_after = PoolRelay::initial_reconnect_duration();
|
||||
|
||||
let should_ping = now - relay.last_ping > self.ping_rate;
|
||||
if should_ping {
|
||||
debug!("pinging {}", relay.relay.url);
|
||||
relay.relay.ping();
|
||||
relay.last_ping = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
RelayStatus::Connecting => {
|
||||
// cool story bro
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to(&mut self, cmd: &ClientMessage, relay_url: &str) {
|
||||
for relay in &mut self.relays {
|
||||
let relay = &mut relay.relay;
|
||||
if relay.url == relay_url {
|
||||
relay.send(cmd);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a websocket url to the RelayPool.
|
||||
pub fn add_url(
|
||||
&mut self,
|
||||
url: String,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<()> {
|
||||
let url = Self::canonicalize_url(url);
|
||||
// Check if the URL already exists in the pool.
|
||||
if self.has(&url) {
|
||||
return Ok(());
|
||||
}
|
||||
let relay = Relay::new(url, wakeup)?;
|
||||
let pool_relay = PoolRelay::new(relay);
|
||||
|
||||
self.relays.push(pool_relay);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_urls(
|
||||
&mut self,
|
||||
urls: BTreeSet<String>,
|
||||
wakeup: impl Fn() + Send + Sync + Clone + 'static,
|
||||
) -> Result<()> {
|
||||
for url in urls {
|
||||
self.add_url(url, wakeup.clone())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_urls(&mut self, urls: &BTreeSet<String>) {
|
||||
self.relays
|
||||
.retain(|pool_relay| !urls.contains(&pool_relay.relay.url));
|
||||
}
|
||||
|
||||
// standardize the format (ie, trailing slashes)
|
||||
fn canonicalize_url(url: String) -> String {
|
||||
match Url::parse(&url) {
|
||||
Ok(parsed_url) => parsed_url.to_string(),
|
||||
Err(_) => url, // If parsing fails, return the original URL.
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to receive a pool event from a list of relays. The
|
||||
/// function searches each relay in the list in order, attempting to
|
||||
/// receive a message from each. If a message is received, return it.
|
||||
/// If no message is received from any relays, None is returned.
|
||||
pub fn try_recv(&mut self) -> Option<PoolEvent<'_>> {
|
||||
for relay in &mut self.relays {
|
||||
let relay = &mut relay.relay;
|
||||
if let Some(event) = relay.receiver.try_recv() {
|
||||
match &event {
|
||||
WsEvent::Opened => {
|
||||
relay.status = RelayStatus::Connected;
|
||||
}
|
||||
WsEvent::Closed => {
|
||||
relay.status = RelayStatus::Disconnected;
|
||||
}
|
||||
WsEvent::Error(err) => {
|
||||
error!("{:?}", err);
|
||||
relay.status = RelayStatus::Disconnected;
|
||||
}
|
||||
WsEvent::Message(ev) => {
|
||||
// let's just handle pongs here.
|
||||
// We only need to do this natively.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let WsMessage::Ping(ref bs) = ev {
|
||||
debug!("pong {}", &relay.url);
|
||||
relay.sender.send(WsMessage::Pong(bs.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(PoolEvent {
|
||||
event,
|
||||
relay: &relay.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user