noteguard

the nostr relay spam guardian
git clone git://jb55.com/noteguard
Log | Files | Refs | README | LICENSE

commit 15e53a116afa74d4393259708f7dab388f1fe9b9
parent 794468d98a43aaa5af44732893910c58c001b2ea
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 11 Jul 2024 09:02:47 -0700

filter: add forwarder filter

Fixes: https://github.com/damus-io/noteguard/issues/10
Changelog-Added: Add forwarder filter
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
MMakefile | 2++
MREADME.md | 12++++++++++++
Anoteguard-forwarder.toml | 8++++++++
Asrc/filters/forwarder.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/filters/mod.rs | 6++++++
Msrc/main.rs | 22++++++++++++++++++++++
Msrc/note_filter.rs | 4++--
Atest/delayed-nostril | 10++++++++++
8 files changed, 169 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,2 +1,4 @@ tags: find src -name '*.rs' | xargs ctags + +.PHONY: tags diff --git a/README.md b/README.md @@ -131,6 +131,18 @@ There are no config options, but an empty config entry is still needed: `[filters.protected_events]` +### Forwarder + +* name: `forwarder` + +The forwarder filter allows you to forward notes to another relay. Notes will +be queued if the connection goes down (up to the `queue_size` buffer limit) + +- `relay` - the relay to forward notes to, eg: `ws://localhost:8080` + +- `queue_size` *optional* - size of the note queue, this is used to buffer notes if the connection goes down + + ## Testing You can test your filters like so: diff --git a/noteguard-forwarder.toml b/noteguard-forwarder.toml @@ -0,0 +1,8 @@ + +pipeline = ["ratelimit", "forwarder"] + +[filters.forwarder] +relay = "ws://127.0.0.1:8080" + +[filters.ratelimit] +posts_per_minute = 3 diff --git a/src/filters/forwarder.rs b/src/filters/forwarder.rs @@ -0,0 +1,107 @@ +use serde::Deserialize; +use crate::{Note, Action, NoteFilter, InputMessage, OutputMessage}; +use futures_util::{SinkExt, StreamExt}; +use tokio::sync::mpsc::{self, Sender, Receiver}; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::WebSocketStream; +use tokio::time::{sleep, timeout, Duration}; +use serde_json::json; +use log::{error, info, debug}; + +#[derive(Default, Deserialize)] +pub struct Forwarder { + relay: String, + + /// the size of our bounded queue + queue_size: Option<u32>, + + /// The channel used for communicating with the forwarder thread + #[serde(skip)] + channel: Option<Sender<Note>>, +} + +async fn client_reconnect(relay: &str) -> WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>> { + loop { + match connect_async(relay).await { + Err(e) => { + error!("failed to connect to relay {}: {}", relay, e); + sleep(Duration::from_secs(5)).await; + continue; + } + Ok((ws, _)) => { + info!("connected to relay: {}", relay); + return ws; + } + } + } +} + +async fn forwarder_task(relay: String, mut rx: Receiver<Note>) { + let stream = client_reconnect(&relay).await; + let (mut writer, mut reader) = stream.split(); + + loop { + tokio::select! { + result = timeout(Duration::from_secs(10), rx.recv()) => { + match result { + Ok(Some(note)) => { + if let Err(e) = writer.send(Message::Text(serde_json::to_string(&json!(["EVENT", note])).unwrap())).await { + error!("got error: '{}', reconnecting...", e); + let (w, r) = client_reconnect(&relay).await.split(); + writer = w; + reader = r; + } + }, + Ok(None) => { + // Channel has been closed, exit the loop + error!("channel closed, stopping forwarder_task"); + break; + } + Err(_) => { + // Timeout occurred, send a ping + // try reading for pongs, etc + let _r = reader.next(); + debug!("timeout reading note queue, sending ping"); + + if let Err(e) = writer.send(Message::Ping(vec![])).await { + error!("error during ping ({}), reconnecting...", e); + let (w, r) = client_reconnect(&relay).await.split(); + writer = w; + reader = r; + } + } + } + } + } + } +} + +impl NoteFilter for Forwarder { + fn name(&self) -> &'static str { + "forwarder" + } + + fn filter_note(&mut self, input: &InputMessage) -> OutputMessage { + if self.channel.is_none() { + let (tx, rx) = mpsc::channel(self.queue_size.unwrap_or(1000) as usize); + let relay = self.relay.clone(); + + tokio::task::spawn(async move { + forwarder_task(relay, rx).await; + }); + + self.channel = Some(tx); + } + + // Add code to process input and send through channel + if let Some(ref channel) = self.channel { + if let Err(e) = channel.try_send(input.event.clone()) { + eprintln!("could not forward note: {}", e); + } + } + + // Create and return an appropriate OutputMessage + OutputMessage::new(input.event.id.clone(), Action::Accept, None) + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs @@ -3,7 +3,13 @@ mod protected_events; mod ratelimit; mod whitelist; +#[cfg(feature = "forwarder")] +mod forwarder; + pub use kinds::Kinds; pub use protected_events::ProtectedEvents; pub use ratelimit::RateLimit; pub use whitelist::Whitelist; + +#[cfg(feature = "forwarder")] +pub use forwarder::Forwarder; diff --git a/src/main.rs b/src/main.rs @@ -1,9 +1,14 @@ use noteguard::filters::{Kinds, ProtectedEvents, RateLimit, Whitelist}; + +#[cfg(feature = "forwarder")] +use noteguard::filters::Forwarder; + use noteguard::{Action, InputMessage, NoteFilter, OutputMessage}; use serde::de::DeserializeOwned; use serde::Deserialize; use std::collections::HashMap; use std::io::{self, BufRead, Read, Write}; +use log::info; #[derive(Deserialize)] struct Config { @@ -44,6 +49,9 @@ impl Noteguard { self.register_filter::<Whitelist>(); self.register_filter::<ProtectedEvents>(); self.register_filter::<Kinds>(); + + #[cfg(feature = "forwarder")] + self.register_filter::<Forwarder>(); } /// Run the loaded filters. You must call `load_config` before calling this, otherwise @@ -94,7 +102,21 @@ impl Noteguard { } } +#[cfg(feature = "forwarder")] +#[tokio::main] +async fn main() { + noteguard(); +} + +#[cfg(not(feature = "forwarder"))] fn main() { + noteguard(); +} + +fn noteguard() { + env_logger::init(); + info!("running noteguard"); + let config_path = "noteguard.toml"; let mut noteguard = Noteguard::new(); diff --git a/src/note_filter.rs b/src/note_filter.rs @@ -1,7 +1,7 @@ use crate::{InputMessage, OutputMessage}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize)] +#[derive(Deserialize, Serialize, Clone)] pub struct Note { pub id: String, pub pubkey: String, diff --git a/test/delayed-nostril b/test/delayed-nostril @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +while true +do + note="$(nostril --silent --content hello)" + echo "{\"type\": \"new\",\"receivedAt\":12345,\"sourceType\":\"IP4\",\"sourceInfo\": \"127.0.0.2\",\"event\":$note}" + + sleep ${1:-0.1} +done +