Compare commits

..
11 Commits
Author SHA1 Message Date
Greg Heartsfield 2e2e01203b build: bump version to 0.1.6 2021-12-23 21:44:12 -06:00
Greg Heartsfield 100f890284 feat: add until for request filters
This implements an additional filter criteria for selecting events
prior to some timestamp.

See https://github.com/fiatjaf/nostr/issues/39x
2021-12-23 21:38:32 -06:00
Greg Heartsfield 0e288fe678 feat: send messages in order of oldest to newest 2021-12-23 21:36:46 -06:00
Greg Heartsfield bfc804e18c feat: debug protocol messages 2021-12-23 21:30:04 -06:00
Greg Heartsfield 8a8ee5c425 build: bump version to 0.1.5 2021-12-19 16:45:17 -06:00
Greg Heartsfield 55bb6bd440 feat: add resource limits for websocket messages 2021-12-19 16:26:32 -06:00
Greg Heartsfield 7933abaa48 fix: allow unknown fields, like author 2021-12-19 16:18:03 -06:00
Greg Heartsfield 9b959e1012 build: bump version to 0.1.4 2021-12-16 19:00:46 -06:00
Greg Heartsfield 5b6a20dfa6 feat: remove author from subscriptions (NIP-01 Spec)
The `authors` field is sufficient to represent all queries that
`author` could have been used in.  See
https://github.com/fiatjaf/nostr/issues/34 for the discussion leading
to this removal.
2021-12-16 18:53:53 -06:00
Greg Heartsfield acf6231277 build: bump version to 0.1.3 2021-12-15 07:56:34 -06:00
Greg Heartsfield 49598b2c9e fix: subscription event filtering bugs
Subscriptions properly filter using the authors tag.  Petname/keys are
correctly filtered (previously the event tags were incorrectly used).
2021-12-14 21:38:26 -06:00
8 changed files with 53 additions and 61 deletions
Generated
+1 -1
View File
@@ -435,7 +435,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.1.0" version = "0.1.6"
dependencies = [ dependencies = [
"bitcoin_hashes", "bitcoin_hashes",
"env_logger", "env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nostr-rs-relay" name = "nostr-rs-relay"
version = "0.1.2" version = "0.1.6"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
+5 -5
View File
@@ -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.
+8 -10
View File
@@ -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
@@ -239,6 +229,12 @@ fn query_from_sub(sub: &Subscription) -> String {
let created_clause = format!("created_at > {}", f.since.unwrap()); let created_clause = format!("created_at > {}", f.since.unwrap());
filter_components.push(created_clause); filter_components.push(created_clause);
} }
// Query for timestamp
if f.until.is_some() {
let until_clause = format!("created_at < {}", f.until.unwrap());
filter_components.push(until_clause);
}
// combine all clauses, and add to filter_clauses // combine all clauses, and add to filter_clauses
if !filter_components.is_empty() { if !filter_components.is_empty() {
let mut fc = "( ".to_owned(); let mut fc = "( ".to_owned();
@@ -256,6 +252,8 @@ fn query_from_sub(sub: &Subscription) -> String {
query.push_str(" WHERE "); query.push_str(" WHERE ");
query.push_str(&filter_clauses.join(" OR ")); query.push_str(&filter_clauses.join(" OR "));
} }
// add order clause
query.push_str(" ORDER BY created_at ASC");
debug!("query string: {}", query); debug!("query string: {}", query);
query query
} }
+5
View File
@@ -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
View File
@@ -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");
+1
View File
@@ -53,6 +53,7 @@ impl Stream for NostrStream {
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
/// Convert Message to NostrMessage /// Convert Message to NostrMessage
fn convert(msg: String) -> Result<NostrMessage> { fn convert(msg: String) -> Result<NostrMessage> {
debug!("raw msg: {}", msg);
let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into()); let parsed_res: Result<NostrMessage> = serde_json::from_str(&msg).map_err(|e| e.into());
match parsed_res { match parsed_res {
Ok(m) => Ok(m), Ok(m) => Ok(m),
+19 -39
View File
@@ -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
@@ -32,6 +29,8 @@ pub struct ReqFilter {
pub pubkey: Option<String>, pub pubkey: Option<String>,
/// Events published after this time /// Events published after this time
pub since: Option<u64>, pub since: Option<u64>,
/// Events published before this time
pub until: Option<u64>,
/// List of author public keys /// List of author public keys
pub authors: Option<Vec<String>>, pub authors: Option<Vec<String>>,
} }
@@ -105,17 +104,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 +121,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 +140,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 +156,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 +258,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 +275,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"]}]"#)?;