Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8ee5c425 | ||
|
|
55bb6bd440 | ||
|
|
7933abaa48 | ||
|
|
9b959e1012 | ||
|
|
5b6a20dfa6 | ||
|
|
acf6231277 | ||
|
|
49598b2c9e |
Generated
+1
-1
@@ -435,7 +435,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.1.0"
|
version = "0.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin_hashes",
|
"bitcoin_hashes",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nostr-rs-relay"
|
name = "nostr-rs-relay"
|
||||||
version = "0.1.2"
|
version = "0.1.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
+5
-5
@@ -45,15 +45,15 @@ impl ClientConn {
|
|||||||
self.client_id.to_string().chars().take(8).collect()
|
self.client_id.to_string().chars().take(8).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the first subscription identifier that matches the event,
|
/// Find all matching subscriptions.
|
||||||
/// if any do.
|
pub fn get_matching_subscriptions(&self, e: &Event) -> Vec<&str> {
|
||||||
pub fn get_matching_subscription(&self, e: &Event) -> Option<&str> {
|
let mut v: Vec<&str> = vec![];
|
||||||
for (id, sub) in self.subscriptions.iter() {
|
for (id, sub) in self.subscriptions.iter() {
|
||||||
if sub.interested_in_event(e) {
|
if sub.interested_in_event(e) {
|
||||||
return Some(id);
|
v.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new subscription for this connection.
|
/// Add a new subscription for this connection.
|
||||||
|
|||||||
@@ -181,16 +181,6 @@ fn query_from_sub(sub: &Subscription) -> String {
|
|||||||
for f in sub.filters.iter() {
|
for f in sub.filters.iter() {
|
||||||
// individual filter components
|
// individual filter components
|
||||||
let mut filter_components: Vec<String> = Vec::new();
|
let mut filter_components: Vec<String> = Vec::new();
|
||||||
// Query for "author"
|
|
||||||
// https://github.com/fiatjaf/nostr/issues/34
|
|
||||||
// I believe the author & authors fields are redundant.
|
|
||||||
if f.author.is_some() {
|
|
||||||
let author_str = f.author.as_ref().unwrap();
|
|
||||||
if is_hex(author_str) {
|
|
||||||
let author_clause = format!("author = x'{}'", author_str);
|
|
||||||
filter_components.push(author_clause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Query for "authors"
|
// Query for "authors"
|
||||||
if f.authors.is_some() {
|
if f.authors.is_some() {
|
||||||
let authors_escaped: Vec<String> = f
|
let authors_escaped: Vec<String> = f
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ impl Event {
|
|||||||
pub fn event_tag_match(&self, eventid: &str) -> bool {
|
pub fn event_tag_match(&self, eventid: &str) -> bool {
|
||||||
self.get_event_tags().contains(&eventid)
|
self.get_event_tags().contains(&eventid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a given event is referenced in an event tag.
|
||||||
|
pub fn pubkey_tag_match(&self, pubkey: &str) -> bool {
|
||||||
|
self.get_pubkey_tags().contains(&pubkey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+13
-5
@@ -18,6 +18,7 @@ use tokio::sync::broadcast;
|
|||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
use tungstenite::protocol::WebSocketConfig;
|
||||||
|
|
||||||
/// Start running a Nostr relay server.
|
/// Start running a Nostr relay server.
|
||||||
fn main() -> Result<(), Error> {
|
fn main() -> Result<(), Error> {
|
||||||
@@ -93,8 +94,15 @@ async fn nostr_server(
|
|||||||
) {
|
) {
|
||||||
// get a broadcast channel for clients to communicate on
|
// get a broadcast channel for clients to communicate on
|
||||||
let mut bcast_rx = broadcast.subscribe();
|
let mut bcast_rx = broadcast.subscribe();
|
||||||
|
// websocket configuration / limits
|
||||||
|
let config = WebSocketConfig {
|
||||||
|
max_send_queue: None,
|
||||||
|
max_message_size: Some(2 << 19), // 512K
|
||||||
|
max_frame_size: Some(2 << 19), // 512k
|
||||||
|
accept_unmasked_frames: false, // follow the spec
|
||||||
|
};
|
||||||
// upgrade the TCP connection to WebSocket
|
// upgrade the TCP connection to WebSocket
|
||||||
let conn = tokio_tungstenite::accept_async(stream).await;
|
let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await;
|
||||||
let ws_stream = conn.expect("websocket handshake error");
|
let ws_stream = conn.expect("websocket handshake error");
|
||||||
// wrap websocket into a stream & sink of Nostr protocol messages
|
// wrap websocket into a stream & sink of Nostr protocol messages
|
||||||
let mut nostr_stream = protostream::wrap_ws_in_nostr(ws_stream);
|
let mut nostr_stream = protostream::wrap_ws_in_nostr(ws_stream);
|
||||||
@@ -129,16 +137,16 @@ async fn nostr_server(
|
|||||||
Ok(global_event) = bcast_rx.recv() => {
|
Ok(global_event) = bcast_rx.recv() => {
|
||||||
// an event has been broadcast to all clients
|
// an event has been broadcast to all clients
|
||||||
// first check if there is a subscription for this event.
|
// first check if there is a subscription for this event.
|
||||||
let sub_name_opt = conn.get_matching_subscription(&global_event);
|
let matching_subs = conn.get_matching_subscriptions(&global_event);
|
||||||
if let Some(sub_name) = sub_name_opt {
|
for s in matching_subs {
|
||||||
// TODO: serialize at broadcast time, instead of
|
// TODO: serialize at broadcast time, instead of
|
||||||
// once for each consumer.
|
// once for each consumer.
|
||||||
if let Ok(event_str) = serde_json::to_string(&global_event) {
|
if let Ok(event_str) = serde_json::to_string(&global_event) {
|
||||||
debug!("sub match: client: {}, sub: {}, event: {}",
|
debug!("sub match: client: {}, sub: {}, event: {}",
|
||||||
cid, sub_name,
|
cid, s,
|
||||||
global_event.get_event_id_prefix());
|
global_event.get_event_id_prefix());
|
||||||
// create an event response and send it
|
// create an event response and send it
|
||||||
let res = EventRes(sub_name.to_owned(),event_str);
|
let res = EventRes(s.to_owned(),event_str);
|
||||||
nostr_stream.send(res).await.ok();
|
nostr_stream.send(res).await.ok();
|
||||||
} else {
|
} else {
|
||||||
warn!("could not convert event to string");
|
warn!("could not convert event to string");
|
||||||
|
|||||||
+17
-39
@@ -16,12 +16,9 @@ pub struct Subscription {
|
|||||||
/// element can be present if it should be used in filtering, or
|
/// element can be present if it should be used in filtering, or
|
||||||
/// absent ([`None`]) if it should be ignored.
|
/// absent ([`None`]) if it should be ignored.
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct ReqFilter {
|
pub struct ReqFilter {
|
||||||
/// Event hash
|
/// Event hash
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
/// Author public key
|
|
||||||
pub author: Option<String>,
|
|
||||||
/// Event kind
|
/// Event kind
|
||||||
pub kind: Option<u64>,
|
pub kind: Option<u64>,
|
||||||
/// Referenced event hash
|
/// Referenced event hash
|
||||||
@@ -105,17 +102,14 @@ impl Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ReqFilter {
|
impl ReqFilter {
|
||||||
/// Check if this filter either matches, or does not care about an author.
|
/// Check for a match within the authors list.
|
||||||
fn author_match(&self, event: &Event) -> bool {
|
// TODO: Ambiguity; what if the array is empty? Should we
|
||||||
|
// consider that the same as null?
|
||||||
|
fn authors_match(&self, event: &Event) -> bool {
|
||||||
self.authors
|
self.authors
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|vs| vs.contains(&event.pubkey.to_owned()))
|
.map(|vs| vs.contains(&event.pubkey.to_owned()))
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
&& self
|
|
||||||
.author
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v == &event.pubkey)
|
|
||||||
.unwrap_or(true)
|
|
||||||
}
|
}
|
||||||
/// Check if this filter either matches, or does not care about the event tags.
|
/// Check if this filter either matches, or does not care about the event tags.
|
||||||
fn event_match(&self, event: &Event) -> bool {
|
fn event_match(&self, event: &Event) -> bool {
|
||||||
@@ -125,6 +119,15 @@ impl ReqFilter {
|
|||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if this filter either matches, or does not care about
|
||||||
|
/// the pubkey/petname tags.
|
||||||
|
fn pubkey_match(&self, event: &Event) -> bool {
|
||||||
|
self.pubkey
|
||||||
|
.as_ref()
|
||||||
|
.map(|t| event.pubkey_tag_match(t))
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this filter either matches, or does not care about the kind.
|
/// Check if this filter either matches, or does not care about the kind.
|
||||||
fn kind_match(&self, kind: u64) -> bool {
|
fn kind_match(&self, kind: u64) -> bool {
|
||||||
self.kind.map(|v| v == kind).unwrap_or(true)
|
self.kind.map(|v| v == kind).unwrap_or(true)
|
||||||
@@ -135,7 +138,8 @@ impl ReqFilter {
|
|||||||
self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
|
self.id.as_ref().map(|v| v == &event.id).unwrap_or(true)
|
||||||
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
|
&& self.since.map(|t| event.created_at > t).unwrap_or(true)
|
||||||
&& self.kind_match(event.kind)
|
&& self.kind_match(event.kind)
|
||||||
&& self.author_match(event)
|
&& self.authors_match(event)
|
||||||
|
&& self.pubkey_match(event)
|
||||||
&& self.event_match(event)
|
&& self.event_match(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,17 +154,7 @@ mod tests {
|
|||||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
let s: Subscription = serde_json::from_str(raw_json)?;
|
||||||
assert_eq!(s.id, "some-id");
|
assert_eq!(s.id, "some-id");
|
||||||
assert_eq!(s.filters.len(), 1);
|
assert_eq!(s.filters.len(), 1);
|
||||||
assert_eq!(s.filters.get(0).unwrap().author, None);
|
assert_eq!(s.filters.get(0).unwrap().authors, None);
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn multi_empty_request_parse() -> Result<()> {
|
|
||||||
let raw_json = r#"["REQ","some-id",{}]"#;
|
|
||||||
let s: Subscription = serde_json::from_str(raw_json)?;
|
|
||||||
assert_eq!(s.id, "some-id");
|
|
||||||
assert_eq!(s.filters.len(), 1);
|
|
||||||
assert_eq!(s.filters.get(0).unwrap().author, None);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,23 +256,6 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn author_single() -> Result<()> {
|
|
||||||
// subscription with a filter for ID
|
|
||||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"author":"abc"}]"#)?;
|
|
||||||
let e = Event {
|
|
||||||
id: "123".to_owned(),
|
|
||||||
pubkey: "abc".to_owned(),
|
|
||||||
created_at: 0,
|
|
||||||
kind: 0,
|
|
||||||
tags: Vec::new(),
|
|
||||||
content: "".to_owned(),
|
|
||||||
sig: "".to_owned(),
|
|
||||||
};
|
|
||||||
assert_eq!(s.interested_in_event(&e), true);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn authors_single() -> Result<()> {
|
fn authors_single() -> Result<()> {
|
||||||
// subscription with a filter for ID
|
// subscription with a filter for ID
|
||||||
@@ -296,6 +273,7 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
|
||||||
fn authors_multi_pubkey() -> Result<()> {
|
fn authors_multi_pubkey() -> Result<()> {
|
||||||
// check for any of a set of authors, against the pubkey
|
// check for any of a set of authors, against the pubkey
|
||||||
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?;
|
let s: Subscription = serde_json::from_str(r#"["REQ","xyz",{"authors":["abc", "bcd"]}]"#)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user