@@ -124,6 +124,9 @@ fn extract_channel_from_filter(filter: &nostr::Filter) -> Option<uuid::Uuid> {
124124// Nostr filter JSON. nostr::Filter silently drops unknown fields during
125125// deserialization, so we extract them from the raw JSON Value first.
126126
127+ const BRIDGE_FEED_MAX_LIMIT : i64 = 100 ;
128+ const BRIDGE_THREAD_MAX_LIMIT : u32 = 500 ;
129+
127130fn extract_before_id ( raw : & Value ) -> Option < Vec < u8 > > {
128131 let hex_str = raw. get ( "before_id" ) ?. as_str ( ) ?;
129132 if hex_str. len ( ) == 64 {
@@ -152,6 +155,13 @@ fn extract_feed_types(raw: &Value) -> Option<Vec<String>> {
152155 }
153156}
154157
158+ fn event_in_accessible_channel ( se : & sprout_core:: StoredEvent , accessible : & [ uuid:: Uuid ] ) -> bool {
159+ match se. channel_id {
160+ Some ( ch_id) => accessible. contains ( & ch_id) ,
161+ None => true ,
162+ }
163+ }
164+
155165// ── POST /events ─────────────────────────────────────────────────────────────
156166
157167/// Submit a signed Nostr event via HTTP bridge (NIP-98 auth).
@@ -274,27 +284,44 @@ pub async fn query_events(
274284 None => continue ,
275285 } ;
276286
277- let limit = filter. limit . map ( |l| ( l as i64 ) . min ( 100 ) ) . unwrap_or ( 20 ) ;
287+ let limit = filter
288+ . limit
289+ . map ( |l| ( l as i64 ) . min ( BRIDGE_FEED_MAX_LIMIT ) )
290+ . unwrap_or ( 20 ) ;
278291 let since = filter
279292 . since
280293 . and_then ( |s| chrono:: DateTime :: from_timestamp ( s. as_secs ( ) as i64 , 0 ) ) ;
281294
295+ let mut seen_types = std:: collections:: HashSet :: new ( ) ;
282296 let mut seen = std:: collections:: HashSet :: new ( ) ;
297+ let mut feed_count = 0i64 ;
283298 for feed_type in & feed_types {
284- let type_events = match feed_type. as_str ( ) {
299+ let canonical = if feed_type == "agent_activity" {
300+ "activity"
301+ } else {
302+ feed_type. as_str ( )
303+ } ;
304+ if !seen_types. insert ( canonical) {
305+ continue ;
306+ }
307+ if feed_count >= limit {
308+ break ;
309+ }
310+ let remaining = limit - feed_count;
311+ let type_events = match canonical {
285312 "mentions" => state
286313 . db
287- . query_feed_mentions ( & pubkey_bytes, & accessible_channels, since, limit )
314+ . query_feed_mentions ( & pubkey_bytes, & accessible_channels, since, remaining )
288315 . await
289316 . map_err ( |e| internal_error ( & format ! ( "feed mentions error: {e}" ) ) ) ?,
290317 "needs_action" => state
291318 . db
292- . query_feed_needs_action ( & pubkey_bytes, & accessible_channels, since, limit )
319+ . query_feed_needs_action ( & pubkey_bytes, & accessible_channels, since, remaining )
293320 . await
294321 . map_err ( |e| internal_error ( & format ! ( "feed needs_action error: {e}" ) ) ) ?,
295- "activity" | "agent_activity" => state
322+ "activity" => state
296323 . db
297- . query_feed_activity ( & accessible_channels, since, limit )
324+ . query_feed_activity ( & accessible_channels, since, remaining )
298325 . await
299326 . map_err ( |e| internal_error ( & format ! ( "feed activity error: {e}" ) ) ) ?,
300327 _ => continue ,
@@ -303,13 +330,12 @@ pub async fn query_events(
303330 if !seen. insert ( se. event . id ) {
304331 continue ;
305332 }
306- if let Some ( ch_id) = se. channel_id {
307- if !accessible_channels. contains ( & ch_id) {
308- continue ;
309- }
333+ if !event_in_accessible_channel ( & se, & accessible_channels) {
334+ continue ;
310335 }
311336 if let Ok ( v) = serde_json:: to_value ( & se. event ) {
312337 events. push ( v) ;
338+ feed_count += 1 ;
313339 }
314340 }
315341 }
@@ -319,6 +345,9 @@ pub async fn query_events(
319345 // ── depth_limit: route thread queries to get_thread_replies ──
320346 let e_tag_key = nostr:: SingleLetterTag :: lowercase ( nostr:: Alphabet :: E ) ;
321347 for ( idx, ( raw, filter) ) in raw_filters. iter ( ) . zip ( filters. iter ( ) ) . enumerate ( ) {
348+ if handled. contains ( & idx) {
349+ continue ;
350+ }
322351 let depth = match extract_depth_limit ( raw) {
323352 Some ( d) => d,
324353 None => continue ,
@@ -343,7 +372,10 @@ pub async fn query_events(
343372 }
344373 }
345374
346- let limit = filter. limit . unwrap_or ( 100 ) . min ( 500 ) as u32 ;
375+ let limit = filter
376+ . limit
377+ . unwrap_or ( 100 )
378+ . min ( BRIDGE_THREAD_MAX_LIMIT as usize ) as u32 ;
347379 let thread_replies = state
348380 . db
349381 . get_thread_replies ( & root_bytes, Some ( depth) , limit, None )
@@ -360,10 +392,8 @@ pub async fn query_events(
360392 . await
361393 . map_err ( |e| internal_error ( & format ! ( "thread fetch error: {e}" ) ) ) ?;
362394 for se in stored {
363- if let Some ( ch_id) = se. channel_id {
364- if !accessible_channels. contains ( & ch_id) {
365- continue ;
366- }
395+ if !event_in_accessible_channel ( & se, & accessible_channels) {
396+ continue ;
367397 }
368398 if let Ok ( v) = serde_json:: to_value ( & se. event ) {
369399 events. push ( v) ;
@@ -390,18 +420,20 @@ pub async fn query_events(
390420 . await ;
391421
392422 if let Some ( bid) = extract_before_id ( raw) {
393- if query. until . is_some ( ) {
394- query. before_id = Some ( bid) ;
423+ if query. until . is_none ( ) {
424+ return Err ( api_error (
425+ StatusCode :: BAD_REQUEST ,
426+ "before_id requires until to be set" ,
427+ ) ) ;
395428 }
429+ query. before_id = Some ( bid) ;
396430 }
397431
398432 match state. db . query_events ( & query) . await {
399433 Ok ( stored_events) => {
400434 for se in stored_events {
401- if let Some ( ch_id) = se. channel_id {
402- if !accessible_channels. contains ( & ch_id) {
403- continue ;
404- }
435+ if !event_in_accessible_channel ( & se, & accessible_channels) {
436+ continue ;
405437 }
406438 if !sprout_core:: filter:: filters_match ( std:: slice:: from_ref ( filter) , & se) {
407439 continue ;
@@ -1030,4 +1062,158 @@ mod tests {
10301062 "channel-scoped hit must be accepted when caller has access to that channel"
10311063 ) ;
10321064 }
1065+
1066+ // ── Custom filter field extractor tests ──
1067+
1068+ #[ test]
1069+ fn extract_before_id_valid_hex ( ) {
1070+ let hex = "a" . repeat ( 64 ) ;
1071+ let raw = serde_json:: json!( { "before_id" : hex } ) ;
1072+ let result = extract_before_id ( & raw ) ;
1073+ assert ! ( result. is_some( ) ) ;
1074+ assert_eq ! ( result. unwrap( ) . len( ) , 32 ) ;
1075+ }
1076+
1077+ #[ test]
1078+ fn extract_before_id_short_hex ( ) {
1079+ let raw = serde_json:: json!( { "before_id" : "a" . repeat( 63 ) } ) ;
1080+ assert ! ( extract_before_id( & raw) . is_none( ) ) ;
1081+ }
1082+
1083+ #[ test]
1084+ fn extract_before_id_long_hex ( ) {
1085+ let raw = serde_json:: json!( { "before_id" : "a" . repeat( 65 ) } ) ;
1086+ assert ! ( extract_before_id( & raw) . is_none( ) ) ;
1087+ }
1088+
1089+ #[ test]
1090+ fn extract_before_id_invalid_hex_chars ( ) {
1091+ let raw = serde_json:: json!( { "before_id" : "z" . repeat( 64 ) } ) ;
1092+ assert ! ( extract_before_id( & raw) . is_none( ) ) ;
1093+ }
1094+
1095+ #[ test]
1096+ fn extract_before_id_absent ( ) {
1097+ let raw = serde_json:: json!( { } ) ;
1098+ assert ! ( extract_before_id( & raw) . is_none( ) ) ;
1099+ }
1100+
1101+ #[ test]
1102+ fn extract_before_id_non_string ( ) {
1103+ let raw = serde_json:: json!( { "before_id" : 12345 } ) ;
1104+ assert ! ( extract_before_id( & raw) . is_none( ) ) ;
1105+ }
1106+
1107+ #[ test]
1108+ fn extract_depth_limit_valid ( ) {
1109+ let raw = serde_json:: json!( { "depth_limit" : 3 } ) ;
1110+ assert_eq ! ( extract_depth_limit( & raw) , Some ( 3 ) ) ;
1111+ }
1112+
1113+ #[ test]
1114+ fn extract_depth_limit_zero ( ) {
1115+ let raw = serde_json:: json!( { "depth_limit" : 0 } ) ;
1116+ assert_eq ! ( extract_depth_limit( & raw) , Some ( 0 ) ) ;
1117+ }
1118+
1119+ #[ test]
1120+ fn extract_depth_limit_u32_max ( ) {
1121+ let raw = serde_json:: json!( { "depth_limit" : u32 :: MAX } ) ;
1122+ assert_eq ! ( extract_depth_limit( & raw) , Some ( u32 :: MAX ) ) ;
1123+ }
1124+
1125+ #[ test]
1126+ fn extract_depth_limit_overflow ( ) {
1127+ let raw = serde_json:: json!( { "depth_limit" : ( u32 :: MAX as u64 ) + 1 } ) ;
1128+ assert ! ( extract_depth_limit( & raw) . is_none( ) ) ;
1129+ }
1130+
1131+ #[ test]
1132+ fn extract_depth_limit_negative ( ) {
1133+ let raw = serde_json:: json!( { "depth_limit" : -1 } ) ;
1134+ assert ! ( extract_depth_limit( & raw) . is_none( ) ) ;
1135+ }
1136+
1137+ #[ test]
1138+ fn extract_depth_limit_absent ( ) {
1139+ let raw = serde_json:: json!( { } ) ;
1140+ assert ! ( extract_depth_limit( & raw) . is_none( ) ) ;
1141+ }
1142+
1143+ #[ test]
1144+ fn extract_depth_limit_float ( ) {
1145+ let raw = serde_json:: json!( { "depth_limit" : 3.5 } ) ;
1146+ assert ! ( extract_depth_limit( & raw) . is_none( ) ) ;
1147+ }
1148+
1149+ #[ test]
1150+ fn extract_feed_types_valid ( ) {
1151+ let raw = serde_json:: json!( { "feed_types" : [ "mentions" , "activity" ] } ) ;
1152+ assert_eq ! (
1153+ extract_feed_types( & raw) ,
1154+ Some ( vec![ "mentions" . to_string( ) , "activity" . to_string( ) ] )
1155+ ) ;
1156+ }
1157+
1158+ #[ test]
1159+ fn extract_feed_types_empty_array ( ) {
1160+ let raw = serde_json:: json!( { "feed_types" : [ ] } ) ;
1161+ assert ! ( extract_feed_types( & raw) . is_none( ) ) ;
1162+ }
1163+
1164+ #[ test]
1165+ fn extract_feed_types_mixed_types ( ) {
1166+ let raw = serde_json:: json!( { "feed_types" : [ "mentions" , 42 , "activity" ] } ) ;
1167+ assert_eq ! (
1168+ extract_feed_types( & raw) ,
1169+ Some ( vec![ "mentions" . to_string( ) , "activity" . to_string( ) ] )
1170+ ) ;
1171+ }
1172+
1173+ #[ test]
1174+ fn extract_feed_types_absent ( ) {
1175+ let raw = serde_json:: json!( { } ) ;
1176+ assert ! ( extract_feed_types( & raw) . is_none( ) ) ;
1177+ }
1178+
1179+ #[ test]
1180+ fn extract_feed_types_non_array ( ) {
1181+ let raw = serde_json:: json!( { "feed_types" : "mentions" } ) ;
1182+ assert ! ( extract_feed_types( & raw) . is_none( ) ) ;
1183+ }
1184+
1185+ #[ test]
1186+ fn event_accessible_no_channel ( ) {
1187+ let keys = Keys :: generate ( ) ;
1188+ let ev = EventBuilder :: new ( Kind :: Custom ( 1 ) , "test" )
1189+ . sign_with_keys ( & keys)
1190+ . unwrap ( ) ;
1191+ let se = sprout_core:: StoredEvent :: new ( ev, None ) ;
1192+ assert ! ( event_in_accessible_channel( & se, & [ ] ) ) ;
1193+ }
1194+
1195+ #[ test]
1196+ fn event_accessible_matching_channel ( ) {
1197+ let keys = Keys :: generate ( ) ;
1198+ let ev = EventBuilder :: new ( Kind :: Custom ( 1 ) , "test" )
1199+ . sign_with_keys ( & keys)
1200+ . unwrap ( ) ;
1201+ let ch = uuid:: Uuid :: new_v4 ( ) ;
1202+ let mut se = sprout_core:: StoredEvent :: new ( ev, None ) ;
1203+ se. channel_id = Some ( ch) ;
1204+ assert ! ( event_in_accessible_channel( & se, & [ ch] ) ) ;
1205+ }
1206+
1207+ #[ test]
1208+ fn event_inaccessible_channel ( ) {
1209+ let keys = Keys :: generate ( ) ;
1210+ let ev = EventBuilder :: new ( Kind :: Custom ( 1 ) , "test" )
1211+ . sign_with_keys ( & keys)
1212+ . unwrap ( ) ;
1213+ let ch = uuid:: Uuid :: new_v4 ( ) ;
1214+ let other = uuid:: Uuid :: new_v4 ( ) ;
1215+ let mut se = sprout_core:: StoredEvent :: new ( ev, None ) ;
1216+ se. channel_id = Some ( ch) ;
1217+ assert ! ( !event_in_accessible_channel( & se, & [ other] ) ) ;
1218+ }
10331219}
0 commit comments