initial nostr code
This commit is contained in:
38
enostr/src/error.rs
Normal file
38
enostr/src/error.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
MessageEmpty,
|
||||
MessageDecodeFailed,
|
||||
InvalidSignature,
|
||||
Json(serde_json::Error),
|
||||
Generic(String),
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Error::MessageEmpty, Error::MessageEmpty) => true,
|
||||
(Error::MessageDecodeFailed, Error::MessageDecodeFailed) => true,
|
||||
(Error::InvalidSignature, Error::InvalidSignature) => true,
|
||||
// This is slightly wrong but whatevs
|
||||
(Error::Json(..), Error::Json(..)) => true,
|
||||
(Error::Generic(left), Error::Generic(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::Eq for Error {}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Generic(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Error::Json(e)
|
||||
}
|
||||
}
|
||||
73
enostr/src/event.rs
Normal file
73
enostr/src/event.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use crate::{Error, Result};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Event is the struct used to represent a Nostr event
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Event {
|
||||
/// 32-bytes sha256 of the the serialized event data
|
||||
pub id: String,
|
||||
/// 32-bytes hex-encoded public key of the event creator
|
||||
#[serde(rename = "pubkey")]
|
||||
pub pubkey: String,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl PartialEq for Event {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Event {}
|
||||
|
||||
impl Event {
|
||||
pub fn from_json(s: &str) -> Result<Self> {
|
||||
serde_json::from_str(s).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn verify(&self) -> Result<Self> {
|
||||
return 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> {
|
||||
let event = Event {
|
||||
id: id.to_string(),
|
||||
pubkey: pubkey.to_string(),
|
||||
created_at,
|
||||
kind,
|
||||
tags,
|
||||
content: content.to_string(),
|
||||
sig: sig.to_string(),
|
||||
};
|
||||
|
||||
event.verify()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for Event {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
Event::from_json(s)
|
||||
}
|
||||
}
|
||||
10
enostr/src/lib.rs
Normal file
10
enostr/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod error;
|
||||
mod event;
|
||||
mod relay;
|
||||
|
||||
pub use error::Error;
|
||||
pub use event::Event;
|
||||
pub use relay::pool::RelayPool;
|
||||
pub use relay::Relay;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||
283
enostr/src/relay/message.rs
Normal file
283
enostr/src/relay/message.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use crate::Error;
|
||||
use crate::Event;
|
||||
use crate::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
use ewebsock::{WsEvent, WsMessage};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct CommandResult {
|
||||
event_id: String,
|
||||
status: bool,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum RelayMessage {
|
||||
OK(CommandResult),
|
||||
Eose(String),
|
||||
Event(String, Event),
|
||||
Notice(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RelayEvent {
|
||||
Opened,
|
||||
Closed,
|
||||
Other(WsMessage),
|
||||
Message(RelayMessage),
|
||||
}
|
||||
|
||||
impl TryFrom<WsEvent> for RelayEvent {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(message: WsEvent) -> Result<Self> {
|
||||
match message {
|
||||
WsEvent::Opened => Ok(RelayEvent::Opened),
|
||||
WsEvent::Closed => Ok(RelayEvent::Closed),
|
||||
WsEvent::Message(ws_msg) => ws_msg.try_into(),
|
||||
WsEvent::Error(s) => Err(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<WsMessage> for RelayEvent {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(wsmsg: WsMessage) -> Result<Self> {
|
||||
match wsmsg {
|
||||
WsMessage::Text(s) => RelayMessage::from_json(&s).map(RelayEvent::Message),
|
||||
wsmsg => Ok(RelayEvent::Other(wsmsg)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelayMessage {
|
||||
pub fn eose(subid: String) -> Self {
|
||||
RelayMessage::Eose(subid)
|
||||
}
|
||||
|
||||
pub fn notice(msg: String) -> Self {
|
||||
RelayMessage::Notice(msg)
|
||||
}
|
||||
|
||||
pub fn ok(event_id: String, status: bool, message: String) -> Self {
|
||||
RelayMessage::OK(CommandResult {
|
||||
event_id: event_id,
|
||||
status,
|
||||
message: message,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn event(ev: Event, sub_id: String) -> Self {
|
||||
RelayMessage::Event(sub_id, ev)
|
||||
}
|
||||
|
||||
// I was lazy and took this from the nostr crate. thx yuki!
|
||||
pub fn from_json(msg: &str) -> Result<Self> {
|
||||
if msg.is_empty() {
|
||||
return Err(Error::MessageEmpty);
|
||||
}
|
||||
|
||||
let v: Vec<Value> = serde_json::from_str(msg).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
// Notice
|
||||
// Relay response format: ["NOTICE", <message>]
|
||||
if v[0] == "NOTICE" {
|
||||
if v.len() != 2 {
|
||||
return Err(Error::MessageDecodeFailed);
|
||||
}
|
||||
let v_notice: String =
|
||||
serde_json::from_value(v[1].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
return Ok(Self::notice(v_notice));
|
||||
}
|
||||
|
||||
// Event
|
||||
// Relay response format: ["EVENT", <subscription id>, <event JSON>]
|
||||
if v[0] == "EVENT" {
|
||||
if v.len() != 3 {
|
||||
return Err(Error::MessageDecodeFailed);
|
||||
}
|
||||
|
||||
let event =
|
||||
Event::from_json(&v[2].to_string()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
let subscription_id: String =
|
||||
serde_json::from_value(v[1].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
return Ok(Self::event(event, subscription_id));
|
||||
}
|
||||
|
||||
// EOSE (NIP-15)
|
||||
// Relay response format: ["EOSE", <subscription_id>]
|
||||
if v[0] == "EOSE" {
|
||||
if v.len() != 2 {
|
||||
return Err(Error::MessageDecodeFailed);
|
||||
}
|
||||
|
||||
let subscription_id: String =
|
||||
serde_json::from_value(v[1].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
return Ok(Self::eose(subscription_id));
|
||||
}
|
||||
|
||||
// OK (NIP-20)
|
||||
// Relay response format: ["OK", <event_id>, <true|false>, <message>]
|
||||
if v[0] == "OK" {
|
||||
if v.len() != 4 {
|
||||
return Err(Error::MessageDecodeFailed);
|
||||
}
|
||||
|
||||
let event_id: String =
|
||||
serde_json::from_value(v[1].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
let status: bool =
|
||||
serde_json::from_value(v[2].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
let message: String =
|
||||
serde_json::from_value(v[3].clone()).map_err(|_| Error::MessageDecodeFailed)?;
|
||||
|
||||
return Ok(Self::ok(event_id, status, message));
|
||||
}
|
||||
|
||||
Err(Error::MessageDecodeFailed)
|
||||
}
|
||||
}
|
||||
|
||||
#[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::MessageDecodeFailed
|
||||
);
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_notice_msg_content).unwrap_err(),
|
||||
Error::MessageDecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_handle_valid_event() -> Result<()> {
|
||||
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);
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(valid_event_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::MessageDecodeFailed
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(invalid_event_msg_content).unwrap_err(),
|
||||
Error::MessageDecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[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::MessageDecodeFailed
|
||||
);
|
||||
|
||||
// The subscription ID is not string
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["EOSE", 404]"#).unwrap_err(),
|
||||
Error::MessageDecodeFailed
|
||||
);
|
||||
}
|
||||
|
||||
#[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::MessageDecodeFailed
|
||||
);
|
||||
|
||||
// Invalid status
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", hello, ""]"#).unwrap_err(),
|
||||
Error::MessageDecodeFailed
|
||||
);
|
||||
|
||||
// Invalid message
|
||||
assert_eq!(
|
||||
RelayMessage::from_json(r#"["OK", "b1a649ebe8b435ec71d3784793f3bbf4b93e64e17568a741aecd4c7ddeafce30", hello, 404]"#).unwrap_err(),
|
||||
Error::MessageDecodeFailed
|
||||
);
|
||||
}
|
||||
}
|
||||
60
enostr/src/relay/mod.rs
Normal file
60
enostr/src/relay/mod.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use ewebsock::{WsReceiver, WsSender};
|
||||
|
||||
use crate::Result;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
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) -> Result<Self> {
|
||||
let status = RelayStatus::Connecting;
|
||||
let (sender, receiver) = ewebsock::connect(&url)?;
|
||||
|
||||
Ok(Self {
|
||||
url,
|
||||
sender,
|
||||
receiver,
|
||||
status,
|
||||
})
|
||||
}
|
||||
}
|
||||
62
enostr/src/relay/pool.rs
Normal file
62
enostr/src/relay/pool.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::relay::message::RelayEvent;
|
||||
use crate::relay::Relay;
|
||||
use crate::Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PoolMessage<'a> {
|
||||
relay: &'a str,
|
||||
event: RelayEvent,
|
||||
}
|
||||
|
||||
pub struct RelayPool {
|
||||
relays: Vec<Relay>,
|
||||
}
|
||||
|
||||
impl Default for RelayPool {
|
||||
fn default() -> RelayPool {
|
||||
RelayPool { relays: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl RelayPool {
|
||||
// Constructs a new, empty RelayPool.
|
||||
pub fn new(relays: Vec<Relay>) -> RelayPool {
|
||||
RelayPool { relays: relays }
|
||||
}
|
||||
|
||||
pub fn has(&self, url: &str) -> bool {
|
||||
for relay in &self.relays {
|
||||
if &relay.url == url {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adds a websocket url to the RelayPool.
|
||||
pub fn add_url(&mut self, url: String) -> Result<()> {
|
||||
let relay = Relay::new(url)?;
|
||||
|
||||
self.relays.push(relay);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<PoolMessage<'_>> {
|
||||
for relay in &self.relays {
|
||||
if let Some(msg) = relay.receiver.try_recv() {
|
||||
if let Ok(event) = msg.try_into() {
|
||||
let pmsg = PoolMessage {
|
||||
event,
|
||||
relay: &relay.url,
|
||||
};
|
||||
return Some(pmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn connect() {}
|
||||
}
|
||||
Reference in New Issue
Block a user