noteguard

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

commit cae0393ed0a55259db97cb419791ed57325a3f29
parent 8e1bb0363f1c7729195a560ba1cd91f5b1c657c8
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 29 Jul 2024 23:56:17 -0500

filter: add content filter

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mnoteguard.toml | 5++++-
Asrc/filters/content.rs | 23+++++++++++++++++++++++
Msrc/filters/mod.rs | 2++
Msrc/main.rs | 66+++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtest/inputs | 1+
5 files changed, 77 insertions(+), 20 deletions(-)

diff --git a/noteguard.toml b/noteguard.toml @@ -1,10 +1,13 @@ -pipeline = ["protected_events", "kinds", "whitelist", "ratelimit"] +pipeline = ["protected_events", "kinds", "content", "whitelist", "ratelimit"] [filters.ratelimit] posts_per_minute = 8 whitelist = ["127.0.0.1"] +[filters.content] +filters = ["https://cdn.nostr.build/i/some-spammy-or-abusive-image-image.png"] + [filters.whitelist] pubkeys = ["16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93"] ips = ["127.0.0.1"] diff --git a/src/filters/content.rs b/src/filters/content.rs @@ -0,0 +1,23 @@ +use crate::{Action, InputMessage, NoteFilter, OutputMessage}; +use serde::Deserialize; + +#[derive(Deserialize, Default)] +pub struct Content { + filters: Vec<String>, +} + +impl NoteFilter for Content { + fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage { + for filter in &self.filters { + if msg.event.content.contains(filter) { + return OutputMessage::new(msg.event.id.clone(), Action::ShadowReject, None); + } + } + + OutputMessage::new(msg.event.id.clone(), Action::Accept, None) + } + + fn name(&self) -> &'static str { + "content" + } +} diff --git a/src/filters/mod.rs b/src/filters/mod.rs @@ -1,3 +1,4 @@ +mod content; mod kinds; mod protected_events; mod ratelimit; @@ -6,6 +7,7 @@ mod whitelist; #[cfg(feature = "forwarder")] mod forwarder; +pub use content::Content; pub use kinds::Kinds; pub use protected_events::ProtectedEvents; pub use ratelimit::RateLimit; diff --git a/src/main.rs b/src/main.rs @@ -1,14 +1,14 @@ -use noteguard::filters::{Kinds, ProtectedEvents, RateLimit, Whitelist}; +use noteguard::filters::{Content, Kinds, ProtectedEvents, RateLimit, Whitelist}; #[cfg(feature = "forwarder")] use noteguard::filters::Forwarder; +use log::info; use noteguard::{Action, InputMessage, NoteFilter, OutputMessage}; use serde::de::DeserializeOwned; use serde::Deserialize; use std::collections::HashMap; use std::io::{self, Read}; -use log::info; #[derive(Deserialize)] struct Config { @@ -49,6 +49,7 @@ impl Noteguard { self.register_filter::<Whitelist>(); self.register_filter::<ProtectedEvents>(); self.register_filter::<Kinds>(); + self.register_filter::<Content>(); #[cfg(feature = "forwarder")] self.register_filter::<Forwarder>(); @@ -117,7 +118,6 @@ fn serialize_output_message(msg: &OutputMessage) -> String { serde_json::to_string(msg).expect("OutputMessage should always serialize correctly") } - fn noteguard() { env_logger::init(); info!("running noteguard"); @@ -157,7 +157,11 @@ fn noteguard() { }; if input_message.message_type != "new" { - let out = OutputMessage::new(input_message.event.id.clone(), Action::Reject, Some("invalid strfry write policy input".to_string())); + let out = OutputMessage::new( + input_message.event.id.clone(), + Action::Reject, + Some("invalid strfry write policy input".to_string()), + ); println!("{}", serialize_output_message(&out)); continue; } @@ -169,7 +173,6 @@ fn noteguard() { } } - #[cfg(test)] mod tests { use super::*; @@ -197,7 +200,11 @@ mod tests { } // Helper function to create a mock OutputMessage - fn create_mock_output_message(event_id: &str, action: Action, msg: Option<&str>) -> OutputMessage { + fn create_mock_output_message( + event_id: &str, + action: Action, + msg: Option<&str>, + ) -> OutputMessage { OutputMessage { id: event_id.to_string(), action, @@ -210,7 +217,9 @@ mod tests { let noteguard = Noteguard::new(); assert!(noteguard.registered_filters.contains_key("ratelimit")); assert!(noteguard.registered_filters.contains_key("whitelist")); - assert!(noteguard.registered_filters.contains_key("protected_events")); + assert!(noteguard + .registered_filters + .contains_key("protected_events")); assert!(noteguard.registered_filters.contains_key("kinds")); } @@ -219,12 +228,15 @@ mod tests { let mut noteguard = Noteguard::new(); // Create a mock config with one filter (RateLimit) - let config: Config = toml::from_str(r#" + let config: Config = toml::from_str( + r#" pipeline = ["ratelimit"] [filters.ratelimit] posts_per_minute = 3 - "#).expect("Failed to parse config"); + "#, + ) + .expect("Failed to parse config"); assert!(noteguard.load_config(&config).is_ok()); assert_eq!(noteguard.loaded_filters.len(), 1); @@ -235,14 +247,19 @@ mod tests { let mut noteguard = Noteguard::new(); // Create a mock config with one filter (RateLimit) - let config: Config = toml::from_str(r#" + let config: Config = toml::from_str( + r#" pipeline = ["ratelimit"] [filters.ratelimit] posts_per_minute = 3 - "#).expect("Failed to parse config"); + "#, + ) + .expect("Failed to parse config"); - noteguard.load_config(&config).expect("Failed to load config"); + noteguard + .load_config(&config) + .expect("Failed to load config"); let input_message = create_mock_input_message("test_event_1", "new"); let output_message = noteguard.run(input_message); @@ -255,13 +272,18 @@ mod tests { let mut noteguard = Noteguard::new(); // Create a mock config with one filter (ProtectedEvents) which will shadow reject the input - let config: Config = toml::from_str(r#" + let config: Config = toml::from_str( + r#" pipeline = ["protected_events"] [filters.protected_events] - "#).expect("Failed to parse config"); + "#, + ) + .expect("Failed to parse config"); - noteguard.load_config(&config).expect("Failed to load config"); + noteguard + .load_config(&config) + .expect("Failed to load config"); let input_message = create_mock_input_message("test_event_3", "new"); let output_message = noteguard.run(input_message); @@ -274,13 +296,18 @@ mod tests { let mut noteguard = Noteguard::new(); // Create a mock config with one filter (Whitelist) which will reject the input - let config: Config = toml::from_str(r#" + let config: Config = toml::from_str( + r#" pipeline = ["whitelist"] [filters.whitelist] pubkeys = ["something"] - "#).expect("Failed to parse config"); + "#, + ) + .expect("Failed to parse config"); - noteguard.load_config(&config).expect("Failed to load config"); + noteguard + .load_config(&config) + .expect("Failed to load config"); let input_message = create_mock_input_message("test_event_2", "new"); let output_message = noteguard.run(input_message); @@ -308,7 +335,8 @@ mod tests { } "#; - let input_message: InputMessage = serde_json::from_str(input_json).expect("Failed to deserialize input message"); + let input_message: InputMessage = + serde_json::from_str(input_json).expect("Failed to deserialize input message"); assert_eq!(input_message.event.id, "test_event_5"); assert_eq!(input_message.message_type, "new"); } diff --git a/test/inputs b/test/inputs @@ -1,3 +1,4 @@ +{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.3","event":{"id":"eabb9491951fc64d85b6cba28a7d53c333c3f1d59779d63abccf9a8f61a0bdef","pubkey":"5f54041530509de28550475bfe73db709609a4ee1e59281527ba81692923418f","created_at":1722305142,"kind":1,"tags":[["t","lightning"],["t","zapping"],["t","introductions"],["t","freefrom"],["t","help"],["t","nostr"],["t","plebchain"],["t","zapathon"],["t","damus"],["t","bitcoin"],["t","anime"],["t","grownostr"],["t","music"],["t","programming"],["t","cat"],["t","dog"],["t","nsfw"],["t","asknostr"],["t","foodstr"],["t","sports"],["t","acg"],["t","game"],["t","movie"],["t","book"],["t","art"],["t","tech"],["t","science"]],"content":"Freefrom is running out of funds and is about to close down. Please scan the QR code to donate.\n Freefrom 资金不足即将倒闭,请扫描二维码捐款\n https://ice.frostsky.com/2024/07/29/9e447d397cac0c27993f0cbcb6efb235.png\n https://cdn.nostr.build/i/270de92fa4e5118a28352720b7f26c360aa2dbfdb1c12517f3e3117a2f9de47a.png\n ","sig":"6fa13bedffb2250e2437af6898ab3b11444eb3aef5fd4b484b25658632d283774186f98a024c225581cd9bbaccb0966fa1360d13fd5671f97c7f7bf515f94166"}} {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.3","event":{"id": "70651d96a2b6b3431cc06b7543249ccd22ab5c203c6aa590b7688f916f252f8f","pubkey": "879d67486027539073d6531d271e3791b15c3e48becbfe4c3727e93355330cc8","created_at": 1720545068,"kind": 1,"tags": [["-"]],"content": "hello there","sig": "21a901e3663bac846493df588ad2185751a5a2826a64c26afb9edce8f9d9344cf00c1ea43016e7faca69da661eadd2731b457a0c31b207ab6ed509a047bf7845"}} {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.3","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.3","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}