diff --git a/src/bin/chorus_cmd.rs b/src/bin/chorus_cmd.rs index 20c45bf..f7a31f0 100644 --- a/src/bin/chorus_cmd.rs +++ b/src/bin/chorus_cmd.rs @@ -1,4 +1,5 @@ use chorus::error::{ChorusError, Error}; +use pocket_db::ScreenResult; use pocket_types::{Filter, Id, Pubkey, Tags}; use std::env; @@ -50,7 +51,8 @@ fn main() -> Result<(), Error> { let mut filter_buffer: [u8; 128] = [0; 128]; let filter = Filter::from_parts(&[], &[pk], &[], tags, None, None, None, &mut filter_buffer)?; - let events = store.find_events(filter, true, 0, 0, |_| true)?; + let (events, _redacted) = + store.find_events(filter, true, 0, 0, |_| ScreenResult::Match)?; for event in events.iter() { store.remove_event(event.id())?; } diff --git a/src/bin/chorus_dump.rs b/src/bin/chorus_dump.rs index f0e9a79..0c8825a 100644 --- a/src/bin/chorus_dump.rs +++ b/src/bin/chorus_dump.rs @@ -1,4 +1,5 @@ use chorus::error::Error; +use pocket_db::ScreenResult; use pocket_types::{Event, Filter}; use std::env; @@ -23,9 +24,9 @@ fn main() -> Result<(), Error> { let mut buffer: [u8; 128] = [0; 128]; let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?; - let screen = |_: &Event| -> bool { true }; + let screen = |_: &Event| -> ScreenResult { ScreenResult::Match }; - let mut events = store.find_events( + let (mut events, _redacted) = store.find_events( filter, config.allow_scraping, config.allow_scrape_if_limited_to, diff --git a/src/bin/chorus_moderate.rs b/src/bin/chorus_moderate.rs index 4850eff..c5fa613 100644 --- a/src/bin/chorus_moderate.rs +++ b/src/bin/chorus_moderate.rs @@ -1,4 +1,5 @@ use chorus::error::Error; +use pocket_db::ScreenResult; use pocket_types::{Event, Filter, Kind}; use std::env; use std::io::Write; @@ -33,9 +34,9 @@ fn main() -> Result<(), Error> { let mut buffer: [u8; 128] = [0; 128]; let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?; - let screen = |_: &Event| -> bool { true }; + let screen = |_: &Event| -> ScreenResult { ScreenResult::Match }; - let mut events = store.find_events( + let (mut events, _redacted) = store.find_events( filter, config.allow_scraping, config.allow_scrape_if_limited_to, diff --git a/src/lib.rs b/src/lib.rs index 1388f82..2e74d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ use hyper_tungstenite::tungstenite; use hyper_tungstenite::{HyperWebsocket, WebSocketStream}; use hyper_util::rt::TokioIo; use neg_storage::NegentropyStorageVector; -use pocket_db::Store; +use pocket_db::{ScreenResult, Store}; use pocket_types::{Id, OwnedFilter, Pubkey}; use speedy::{Readable, Writable}; use std::borrow::Cow; @@ -482,15 +482,21 @@ impl WebSocketService { 'subs: for (subid, filters) in self.subscriptions.iter() { for filter in filters.iter() { - if filter.event_matches(event)? - && nostr::screen_outgoing_event(event, &event_flags, authorized_user) - { - let message = NostrReply::Event(subid, event); - // note, this is not currently counted in throttling - self.websocket - .send(Message::text(message.as_json())) - .await?; - continue 'subs; + if filter.event_matches(event)? { + let screen_result = + nostr::screen_outgoing_event(event, &event_flags, authorized_user); + if screen_result == ScreenResult::Redacted { + // TBD: Update subscription so the final close can + // let them know there were redactions from + // the post-EOSE data + } else if screen_result == ScreenResult::Match { + let message = NostrReply::Event(subid, event); + // note, this is not currently counted in throttling + self.websocket + .send(Message::text(message.as_json())) + .await?; + continue 'subs; + } } } } diff --git a/src/nostr.rs b/src/nostr.rs index 8f28673..2df9a97 100644 --- a/src/nostr.rs +++ b/src/nostr.rs @@ -5,6 +5,7 @@ use crate::reply::{NostrReply, NostrReplyPrefix}; use crate::WebSocketService; use hyper_tungstenite::tungstenite::Message; use negentropy::{Bytes, Negentropy}; +use pocket_db::ScreenResult; use pocket_types::json::{eat_whitespace, json_unescape, verify_char}; use pocket_types::{read_hex, Event, Filter, Hll8, Kind, OwnedFilter, Pubkey, Time}; use url::Url; @@ -148,6 +149,8 @@ impl WebSocketService { let completes = filters.iter().all(|f| f.completes()); + let mut redacted: bool = false; + // NOTE on private events (DMs, GiftWraps) // As seen above, we will send CLOSED auth-required if they ask for DMs and are not // AUTHed yet. @@ -159,11 +162,11 @@ impl WebSocketService { let mut events: Vec<&Event> = Vec::new(); for filter in filters.iter() { - let screen = |event: &Event| { + let screen = |event: &Event| -> ScreenResult { let event_flags = event_flags(event, &user); screen_outgoing_event(event, &event_flags, authorized_user) }; - let filter_events = { + let (filter_events, was_redacted) = { let config = &*GLOBALS.config.read(); GLOBALS.store.get().unwrap().find_events( filter, @@ -174,6 +177,7 @@ impl WebSocketService { )? }; events.extend(filter_events); + redacted = redacted || was_redacted; } // sort @@ -204,7 +208,15 @@ impl WebSocketService { if completes { // Closed - let reply = NostrReply::Closed(subid, NostrReplyPrefix::None, "".to_owned()); + let reply = if redacted { + NostrReply::Closed(subid, NostrReplyPrefix::None, "".to_owned()) + } else { + NostrReply::Closed( + subid, + NostrReplyPrefix::Redacted, + "Some matching events could not be served to you.".to_owned(), + ) + }; self.send(Message::text(reply.as_json())).await?; } else { // EOSE @@ -531,11 +543,11 @@ impl WebSocketService { // Find all matching events let mut events: Vec<&Event> = Vec::new(); - let screen = |event: &Event| { + let screen = |event: &Event| -> ScreenResult { let event_flags = event_flags(event, &user); screen_outgoing_event(event, &event_flags, authorized_user) }; - let filter_events = { + let (filter_events, _redacted) = { let config = &*GLOBALS.config.read(); GLOBALS.store.get().unwrap().find_events( &filter, @@ -775,59 +787,63 @@ pub fn screen_outgoing_event( event: &Event, event_flags: &EventFlags, authorized_user: bool, -) -> bool { +) -> ScreenResult { // Forbid if it is a private event (DM or GiftWrap) and theey are neither the recipient // nor the author if event.kind() == Kind::from(4) || event.kind() == Kind::from(1059) { - return event_flags.tags_current_user || event_flags.author_is_current_user; + if event_flags.tags_current_user || event_flags.author_is_current_user { + return ScreenResult::Match; + } else { + return ScreenResult::Redacted; + } } // Forbid (and delete) if it has an expired expiration tag if matches!(event.is_expired(), Ok(true)) { let _ = GLOBALS.store.get().unwrap().remove_event(event.id()); - return false; + return ScreenResult::Mismatch; } // Allow if an open relay if GLOBALS.config.read().open_relay { - return true; + return ScreenResult::Match; } // Allow Relay Lists if GLOBALS.config.read().serve_relay_lists && (event.kind() == Kind::from(10002) || event.kind() == Kind::from(10050)) { - return true; + return ScreenResult::Match; } // Allow if event kind ephemeral if event.kind().is_ephemeral() && GLOBALS.config.read().serve_ephemeral { - return true; + return ScreenResult::Match; } // Allow if an authorized_user is asking if authorized_user { - return true; + return ScreenResult::Match; } // Everybody can see events from our authorized users if event_flags.author_is_an_authorized_user { - return true; + return ScreenResult::Match; } // Allow if event is explicitly approved if let Ok(Some(true)) = crate::get_event_approval(GLOBALS.store.get().unwrap(), event.id()) { - return true; + return ScreenResult::Match; } // Allow if author is explicitly approved if let Ok(Some(true)) = crate::get_pubkey_approval(GLOBALS.store.get().unwrap(), event.pubkey()) { - return true; + return ScreenResult::Match; } // Do not allow the rest - false + ScreenResult::Redacted } pub struct EventFlags { diff --git a/src/reply.rs b/src/reply.rs index 695d6a0..fdcde24 100644 --- a/src/reply.rs +++ b/src/reply.rs @@ -9,6 +9,7 @@ pub enum NostrReplyPrefix { Duplicate, Blocked, RateLimited, + Redacted, Restricted, Invalid, Error, @@ -23,6 +24,7 @@ impl fmt::Display for NostrReplyPrefix { NostrReplyPrefix::Duplicate => write!(f, "duplicate: "), NostrReplyPrefix::Blocked => write!(f, "blocked: "), NostrReplyPrefix::RateLimited => write!(f, "rate-limited: "), + NostrReplyPrefix::Redacted => write!(f, "redacted: "), NostrReplyPrefix::Restricted => write!(f, "restricted: "), NostrReplyPrefix::Invalid => write!(f, "invalid: "), NostrReplyPrefix::Error => write!(f, "error: "), diff --git a/src/web/management/mod.rs b/src/web/management/mod.rs index 542274a..a711bb3 100644 --- a/src/web/management/mod.rs +++ b/src/web/management/mod.rs @@ -5,6 +5,7 @@ use http_body_util::combinators::BoxBody; use http_body_util::{BodyExt, Full}; use hyper::body::{Bytes, Incoming}; use hyper::{Request, Response, StatusCode}; +use pocket_db::ScreenResult; use pocket_types::{Event, Filter, Id, Kind, Pubkey}; use serde::Serialize; use serde_json::{json, Map, Value}; @@ -340,15 +341,21 @@ pub fn handle_inner(pubkey: Pubkey, command: Value) -> Result, Err let (_incount, _outcount, filter) = Filter::from_json(b"{}", &mut buffer)?; filter }; - let screen = |e: &Event| -> bool { - !allowed_kinds.contains(&e.kind()) - && !e.kind().is_ephemeral() - && !crate::is_authorized_user(e.pubkey()) + let screen = |e: &Event| -> ScreenResult { + if allowed_kinds.contains(&e.kind()) { + ScreenResult::Mismatch + } else if e.kind().is_ephemeral() { + ScreenResult::Mismatch + } else if crate::is_authorized_user(e.pubkey()) { + ScreenResult::Mismatch + } else { + ScreenResult::Match + } }; let mut need_moderation: Vec = Vec::new(); - let mut events = GLOBALS + let (mut events, _redacted) = GLOBALS .store .get() .unwrap()