diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:51:57 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:51:57 +0000 |
| commit | c8ab2c9c294ae9401ff542d0eecc6606b7908412 (patch) | |
| tree | 2ecf96e0265c855940df149781a0a24640408e1e /src/config.rs | |
| parent | 70c577f10bbe150b6b13bec545dc8720ad005a64 (diff) | |
feat(config): add event blacklist to block all events from specific authors
Adds NGIT_EVENT_BLACKLIST option for blocking all events from specific npubs,
taking precedence over all other validation to enable comprehensive moderation
without affecting curation policy.
Key features:
- Simple npub-only format: <npub>,<npub>,...
- Checked FIRST before any other validation (including repository blacklist)
- Blocks ALL event types (announcements, state events, PRs, comments, etc.)
- Events never reach relay storage or purgatory
- Specific rejection reason for operator debugging
Implementation:
- Add EventBlacklistConfig struct with check() method
- Add NGIT_EVENT_BLACKLIST config option and event_blacklist_config() method
- Add config field to PolicyContext for policy access
- Add check_event_blacklist() to Nip34WritePolicy
- Check event blacklist first in admit_event() method (before any other validation)
- 4 new unit tests covering all blacklist behavior
Configuration synced across all four sources:
- src/config.rs: Core implementation with EventBlacklistConfig
- .env.example: Comprehensive documentation with examples
- docs/reference/configuration.md: Complete reference documentation
- nix/module.nix: NixOS module option with environment mapping
README updates:
- Add comprehensive "Curation & Moderation" section
- Document repository whitelists (GRASP-01 and GRASP-05 modes)
- Document repository and event blacklists with precedence order
- Add configuration table for all curation/moderation settings
- Provide real-world examples for different relay configurations
Testing:
- 4 new tests for event blacklist functionality
- All 336 library tests passing
- All 64 integration tests passing
- All 38 filter support tests passing
Verification:
- Repository blacklist confirmed to apply to sync (uses same admit_event flow)
- Sync events validated through process_event_static -> write_policy.admit_event
Use cases:
- Block spam/abusive users completely
- Prevent malicious actors from submitting any events
- Temporary blocks for investigation
- Moderation without affecting whitelist curation policy
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs index 5f8cbca..a5e4344 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -244,6 +244,42 @@ impl Default for BlacklistConfig { | |||
| 244 | } | 244 | } |
| 245 | } | 245 | } |
| 246 | 246 | ||
| 247 | /// Event blacklist configuration for blocking events by author npub | ||
| 248 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 249 | pub struct EventBlacklistConfig { | ||
| 250 | /// Blacklisted npubs - events from these authors are rejected | ||
| 251 | /// | ||
| 252 | /// If empty, no events are blacklisted by author. | ||
| 253 | /// Applies to ALL event types, preventing events from reaching both the relay and purgatory. | ||
| 254 | pub blacklisted_npubs: Vec<String>, | ||
| 255 | } | ||
| 256 | |||
| 257 | impl EventBlacklistConfig { | ||
| 258 | /// Check if event blacklist is enabled (non-empty blacklist) | ||
| 259 | pub fn enabled(&self) -> bool { | ||
| 260 | !self.blacklisted_npubs.is_empty() | ||
| 261 | } | ||
| 262 | |||
| 263 | /// Check if an event author is blacklisted | ||
| 264 | /// | ||
| 265 | /// Returns Some(reason) if blacklisted, None if not blacklisted. | ||
| 266 | pub fn check(&self, npub: &str) -> Option<String> { | ||
| 267 | if self.blacklisted_npubs.contains(&npub.to_string()) { | ||
| 268 | Some(format!("Event author {} is blacklisted", npub)) | ||
| 269 | } else { | ||
| 270 | None | ||
| 271 | } | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | impl Default for EventBlacklistConfig { | ||
| 276 | fn default() -> Self { | ||
| 277 | Self { | ||
| 278 | blacklisted_npubs: Vec::new(), | ||
| 279 | } | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 247 | /// Database backend type for the relay | 283 | /// Database backend type for the relay |
| 248 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] | 284 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, ValueEnum)] |
| 249 | #[serde(rename_all = "lowercase")] | 285 | #[serde(rename_all = "lowercase")] |
| @@ -428,6 +464,11 @@ pub struct Config { | |||
| 428 | /// Blacklist takes precedence over all whitelists (archive and repository) | 464 | /// Blacklist takes precedence over all whitelists (archive and repository) |
| 429 | #[arg(long, env = "NGIT_REPOSITORY_BLACKLIST", default_value = "")] | 465 | #[arg(long, env = "NGIT_REPOSITORY_BLACKLIST", default_value = "")] |
| 430 | pub repository_blacklist: String, | 466 | pub repository_blacklist: String, |
| 467 | |||
| 468 | /// Event blacklist: comma-separated list of npubs whose events are rejected | ||
| 469 | /// All events from these authors are blocked from both relay storage and purgatory | ||
| 470 | #[arg(long, env = "NGIT_EVENT_BLACKLIST", default_value = "")] | ||
| 471 | pub event_blacklist: String, | ||
| 431 | } | 472 | } |
| 432 | 473 | ||
| 433 | impl Config { | 474 | impl Config { |
| @@ -612,6 +653,20 @@ impl Config { | |||
| 612 | BlacklistConfig { blacklist } | 653 | BlacklistConfig { blacklist } |
| 613 | } | 654 | } |
| 614 | 655 | ||
| 656 | /// Get parsed event blacklist configuration | ||
| 657 | /// | ||
| 658 | /// This method assumes config has been validated - call Config::validate() first! | ||
| 659 | pub fn event_blacklist_config(&self) -> EventBlacklistConfig { | ||
| 660 | let blacklisted_npubs: Vec<String> = self | ||
| 661 | .event_blacklist | ||
| 662 | .split(',') | ||
| 663 | .map(|s| s.trim()) | ||
| 664 | .filter(|s| !s.is_empty()) | ||
| 665 | .map(|s| s.to_string()) | ||
| 666 | .collect(); | ||
| 667 | EventBlacklistConfig { blacklisted_npubs } | ||
| 668 | } | ||
| 669 | |||
| 615 | /// Create config for testing | 670 | /// Create config for testing |
| 616 | #[cfg(test)] | 671 | #[cfg(test)] |
| 617 | pub fn for_testing() -> Self { | 672 | pub fn for_testing() -> Self { |
| @@ -647,6 +702,7 @@ impl Config { | |||
| 647 | archive_read_only: None, | 702 | archive_read_only: None, |
| 648 | repository_whitelist: String::new(), | 703 | repository_whitelist: String::new(), |
| 649 | repository_blacklist: String::new(), | 704 | repository_blacklist: String::new(), |
| 705 | event_blacklist: String::new(), | ||
| 650 | } | 706 | } |
| 651 | } | 707 | } |
| 652 | } | 708 | } |
| @@ -1248,4 +1304,58 @@ mod tests { | |||
| 1248 | let result = config.check(&test_npub, "allowed-repo"); | 1304 | let result = config.check(&test_npub, "allowed-repo"); |
| 1249 | assert!(result.is_none()); | 1305 | assert!(result.is_none()); |
| 1250 | } | 1306 | } |
| 1307 | |||
| 1308 | #[test] | ||
| 1309 | fn test_event_blacklist_config_parsing() { | ||
| 1310 | let keys1 = Keys::generate(); | ||
| 1311 | let keys2 = Keys::generate(); | ||
| 1312 | let npub1 = keys1.public_key().to_bech32().unwrap(); | ||
| 1313 | let npub2 = keys2.public_key().to_bech32().unwrap(); | ||
| 1314 | let config = Config { | ||
| 1315 | event_blacklist: format!("{},{}", npub1, npub2), | ||
| 1316 | ..Config::for_testing() | ||
| 1317 | }; | ||
| 1318 | let event_blacklist_config = config.event_blacklist_config(); | ||
| 1319 | assert_eq!(event_blacklist_config.blacklisted_npubs.len(), 2); | ||
| 1320 | assert!(event_blacklist_config.enabled()); | ||
| 1321 | assert!(event_blacklist_config.blacklisted_npubs.contains(&npub1)); | ||
| 1322 | assert!(event_blacklist_config.blacklisted_npubs.contains(&npub2)); | ||
| 1323 | } | ||
| 1324 | |||
| 1325 | #[test] | ||
| 1326 | fn test_event_blacklist_config_empty() { | ||
| 1327 | let config = Config::for_testing(); | ||
| 1328 | let event_blacklist_config = config.event_blacklist_config(); | ||
| 1329 | assert!(event_blacklist_config.blacklisted_npubs.is_empty()); | ||
| 1330 | assert!(!event_blacklist_config.enabled()); | ||
| 1331 | } | ||
| 1332 | |||
| 1333 | #[test] | ||
| 1334 | fn test_event_blacklist_check_blacklisted() { | ||
| 1335 | let keys = Keys::generate(); | ||
| 1336 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 1337 | let config = EventBlacklistConfig { | ||
| 1338 | blacklisted_npubs: vec![test_npub.clone()], | ||
| 1339 | }; | ||
| 1340 | |||
| 1341 | let result = config.check(&test_npub); | ||
| 1342 | assert!(result.is_some()); | ||
| 1343 | let reason = result.unwrap(); | ||
| 1344 | assert!(reason.contains("author")); | ||
| 1345 | assert!(reason.contains(&test_npub)); | ||
| 1346 | } | ||
| 1347 | |||
| 1348 | #[test] | ||
| 1349 | fn test_event_blacklist_check_not_blacklisted() { | ||
| 1350 | let keys1 = Keys::generate(); | ||
| 1351 | let keys2 = Keys::generate(); | ||
| 1352 | let banned_npub = keys1.public_key().to_bech32().unwrap(); | ||
| 1353 | let allowed_npub = keys2.public_key().to_bech32().unwrap(); | ||
| 1354 | let config = EventBlacklistConfig { | ||
| 1355 | blacklisted_npubs: vec![banned_npub], | ||
| 1356 | }; | ||
| 1357 | |||
| 1358 | let result = config.check(&allowed_npub); | ||
| 1359 | assert!(result.is_none()); | ||
| 1360 | } | ||
| 1251 | } | 1361 | } |