├── .gitignore ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── README.md ├── noteguard-forwarder.toml ├── noteguard.toml ├── src ├── filters │ ├── blacklist.rs │ ├── content.rs │ ├── forwarder.rs │ ├── kinds.rs │ ├── mod.rs │ ├── protected_events.rs │ ├── ratelimit.rs │ └── whitelist.rs ├── lib.rs ├── main.rs ├── messages.rs └── note_filter.rs └── test ├── delayed ├── delayed-nostril └── inputs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .direnv/ 3 | .buildcmd 4 | .build-result 5 | shell.nix 6 | .envrc 7 | tags 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright 2024 Damus Nostr, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the “Software”), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.14" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.4" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" 64 | dependencies = [ 65 | "windows-sys 0.52.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.3" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys 0.52.0", 76 | ] 77 | 78 | [[package]] 79 | name = "autocfg" 80 | version = "1.3.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 83 | 84 | [[package]] 85 | name = "backtrace" 86 | version = "0.3.73" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 89 | dependencies = [ 90 | "addr2line", 91 | "cc", 92 | "cfg-if", 93 | "libc", 94 | "miniz_oxide", 95 | "object", 96 | "rustc-demangle", 97 | ] 98 | 99 | [[package]] 100 | name = "bitflags" 101 | version = "2.6.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 104 | 105 | [[package]] 106 | name = "block-buffer" 107 | version = "0.10.4" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 110 | dependencies = [ 111 | "generic-array", 112 | ] 113 | 114 | [[package]] 115 | name = "byteorder" 116 | version = "1.5.0" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 119 | 120 | [[package]] 121 | name = "bytes" 122 | version = "1.6.0" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 125 | 126 | [[package]] 127 | name = "cc" 128 | version = "1.1.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "eaff6f8ce506b9773fa786672d63fc7a191ffea1be33f72bbd4aeacefca9ffc8" 131 | 132 | [[package]] 133 | name = "cfg-if" 134 | version = "1.0.0" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 137 | 138 | [[package]] 139 | name = "colorchoice" 140 | version = "1.0.1" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" 143 | 144 | [[package]] 145 | name = "core-foundation" 146 | version = "0.9.4" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 149 | dependencies = [ 150 | "core-foundation-sys", 151 | "libc", 152 | ] 153 | 154 | [[package]] 155 | name = "core-foundation-sys" 156 | version = "0.8.7" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 159 | 160 | [[package]] 161 | name = "cpufeatures" 162 | version = "0.2.12" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 165 | dependencies = [ 166 | "libc", 167 | ] 168 | 169 | [[package]] 170 | name = "crypto-common" 171 | version = "0.1.6" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 174 | dependencies = [ 175 | "generic-array", 176 | "typenum", 177 | ] 178 | 179 | [[package]] 180 | name = "data-encoding" 181 | version = "2.6.0" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" 184 | 185 | [[package]] 186 | name = "digest" 187 | version = "0.10.7" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 190 | dependencies = [ 191 | "block-buffer", 192 | "crypto-common", 193 | ] 194 | 195 | [[package]] 196 | name = "env_filter" 197 | version = "0.1.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" 200 | dependencies = [ 201 | "log", 202 | "regex", 203 | ] 204 | 205 | [[package]] 206 | name = "env_logger" 207 | version = "0.11.3" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" 210 | dependencies = [ 211 | "anstream", 212 | "anstyle", 213 | "env_filter", 214 | "humantime", 215 | "log", 216 | ] 217 | 218 | [[package]] 219 | name = "errno" 220 | version = "0.3.9" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 223 | dependencies = [ 224 | "libc", 225 | "windows-sys 0.52.0", 226 | ] 227 | 228 | [[package]] 229 | name = "fastrand" 230 | version = "2.1.0" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" 233 | 234 | [[package]] 235 | name = "fnv" 236 | version = "1.0.7" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 239 | 240 | [[package]] 241 | name = "foreign-types" 242 | version = "0.3.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 245 | dependencies = [ 246 | "foreign-types-shared", 247 | ] 248 | 249 | [[package]] 250 | name = "foreign-types-shared" 251 | version = "0.1.1" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 254 | 255 | [[package]] 256 | name = "futures-core" 257 | version = "0.3.30" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 260 | 261 | [[package]] 262 | name = "futures-macro" 263 | version = "0.3.30" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 266 | dependencies = [ 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | ] 271 | 272 | [[package]] 273 | name = "futures-sink" 274 | version = "0.3.30" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 277 | 278 | [[package]] 279 | name = "futures-task" 280 | version = "0.3.30" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 283 | 284 | [[package]] 285 | name = "futures-util" 286 | version = "0.3.30" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 289 | dependencies = [ 290 | "futures-core", 291 | "futures-macro", 292 | "futures-sink", 293 | "futures-task", 294 | "pin-project-lite", 295 | "pin-utils", 296 | "slab", 297 | ] 298 | 299 | [[package]] 300 | name = "generic-array" 301 | version = "0.14.7" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 304 | dependencies = [ 305 | "typenum", 306 | "version_check", 307 | ] 308 | 309 | [[package]] 310 | name = "getrandom" 311 | version = "0.2.15" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 314 | dependencies = [ 315 | "cfg-if", 316 | "libc", 317 | "wasi", 318 | ] 319 | 320 | [[package]] 321 | name = "gimli" 322 | version = "0.29.0" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 325 | 326 | [[package]] 327 | name = "hermit-abi" 328 | version = "0.3.9" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 331 | 332 | [[package]] 333 | name = "http" 334 | version = "1.1.0" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 337 | dependencies = [ 338 | "bytes", 339 | "fnv", 340 | "itoa", 341 | ] 342 | 343 | [[package]] 344 | name = "httparse" 345 | version = "1.9.4" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" 348 | 349 | [[package]] 350 | name = "humantime" 351 | version = "2.1.0" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 354 | 355 | [[package]] 356 | name = "ipnetwork" 357 | version = "0.20.0" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" 360 | dependencies = [ 361 | "serde", 362 | ] 363 | 364 | [[package]] 365 | name = "is_terminal_polyfill" 366 | version = "1.70.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" 369 | 370 | [[package]] 371 | name = "itoa" 372 | version = "1.0.11" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 375 | 376 | [[package]] 377 | name = "libc" 378 | version = "0.2.155" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 381 | 382 | [[package]] 383 | name = "linux-raw-sys" 384 | version = "0.4.14" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 387 | 388 | [[package]] 389 | name = "log" 390 | version = "0.4.22" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 393 | 394 | [[package]] 395 | name = "memchr" 396 | version = "2.7.4" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 399 | 400 | [[package]] 401 | name = "miniz_oxide" 402 | version = "0.7.4" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 405 | dependencies = [ 406 | "adler", 407 | ] 408 | 409 | [[package]] 410 | name = "mio" 411 | version = "0.8.11" 412 | source = "registry+https://github.com/rust-lang/crates.io-index" 413 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 414 | dependencies = [ 415 | "libc", 416 | "wasi", 417 | "windows-sys 0.48.0", 418 | ] 419 | 420 | [[package]] 421 | name = "native-tls" 422 | version = "0.2.12" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 425 | dependencies = [ 426 | "libc", 427 | "log", 428 | "openssl", 429 | "openssl-probe", 430 | "openssl-sys", 431 | "schannel", 432 | "security-framework", 433 | "security-framework-sys", 434 | "tempfile", 435 | ] 436 | 437 | [[package]] 438 | name = "noteguard" 439 | version = "0.1.0" 440 | dependencies = [ 441 | "env_logger", 442 | "futures-util", 443 | "ipnetwork", 444 | "log", 445 | "serde", 446 | "serde_json", 447 | "tokio", 448 | "tokio-tungstenite", 449 | "toml", 450 | ] 451 | 452 | [[package]] 453 | name = "num_cpus" 454 | version = "1.16.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 457 | dependencies = [ 458 | "hermit-abi", 459 | "libc", 460 | ] 461 | 462 | [[package]] 463 | name = "object" 464 | version = "0.36.1" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" 467 | dependencies = [ 468 | "memchr", 469 | ] 470 | 471 | [[package]] 472 | name = "once_cell" 473 | version = "1.19.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 476 | 477 | [[package]] 478 | name = "openssl" 479 | version = "0.10.66" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 482 | dependencies = [ 483 | "bitflags", 484 | "cfg-if", 485 | "foreign-types", 486 | "libc", 487 | "once_cell", 488 | "openssl-macros", 489 | "openssl-sys", 490 | ] 491 | 492 | [[package]] 493 | name = "openssl-macros" 494 | version = "0.1.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 497 | dependencies = [ 498 | "proc-macro2", 499 | "quote", 500 | "syn", 501 | ] 502 | 503 | [[package]] 504 | name = "openssl-probe" 505 | version = "0.1.5" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 508 | 509 | [[package]] 510 | name = "openssl-sys" 511 | version = "0.9.103" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 514 | dependencies = [ 515 | "cc", 516 | "libc", 517 | "pkg-config", 518 | "vcpkg", 519 | ] 520 | 521 | [[package]] 522 | name = "pin-project-lite" 523 | version = "0.2.14" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 526 | 527 | [[package]] 528 | name = "pin-utils" 529 | version = "0.1.0" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 532 | 533 | [[package]] 534 | name = "pkg-config" 535 | version = "0.3.30" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 538 | 539 | [[package]] 540 | name = "ppv-lite86" 541 | version = "0.2.17" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 544 | 545 | [[package]] 546 | name = "proc-macro2" 547 | version = "1.0.86" 548 | source = "registry+https://github.com/rust-lang/crates.io-index" 549 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 550 | dependencies = [ 551 | "unicode-ident", 552 | ] 553 | 554 | [[package]] 555 | name = "quote" 556 | version = "1.0.36" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 559 | dependencies = [ 560 | "proc-macro2", 561 | ] 562 | 563 | [[package]] 564 | name = "rand" 565 | version = "0.8.5" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 568 | dependencies = [ 569 | "libc", 570 | "rand_chacha", 571 | "rand_core", 572 | ] 573 | 574 | [[package]] 575 | name = "rand_chacha" 576 | version = "0.3.1" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 579 | dependencies = [ 580 | "ppv-lite86", 581 | "rand_core", 582 | ] 583 | 584 | [[package]] 585 | name = "rand_core" 586 | version = "0.6.4" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 589 | dependencies = [ 590 | "getrandom", 591 | ] 592 | 593 | [[package]] 594 | name = "regex" 595 | version = "1.10.5" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" 598 | dependencies = [ 599 | "aho-corasick", 600 | "memchr", 601 | "regex-automata", 602 | "regex-syntax", 603 | ] 604 | 605 | [[package]] 606 | name = "regex-automata" 607 | version = "0.4.7" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 610 | dependencies = [ 611 | "aho-corasick", 612 | "memchr", 613 | "regex-syntax", 614 | ] 615 | 616 | [[package]] 617 | name = "regex-syntax" 618 | version = "0.8.4" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 621 | 622 | [[package]] 623 | name = "rustc-demangle" 624 | version = "0.1.24" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 627 | 628 | [[package]] 629 | name = "rustix" 630 | version = "0.38.34" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" 633 | dependencies = [ 634 | "bitflags", 635 | "errno", 636 | "libc", 637 | "linux-raw-sys", 638 | "windows-sys 0.52.0", 639 | ] 640 | 641 | [[package]] 642 | name = "ryu" 643 | version = "1.0.18" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 646 | 647 | [[package]] 648 | name = "schannel" 649 | version = "0.1.23" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 652 | dependencies = [ 653 | "windows-sys 0.52.0", 654 | ] 655 | 656 | [[package]] 657 | name = "security-framework" 658 | version = "2.11.1" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 661 | dependencies = [ 662 | "bitflags", 663 | "core-foundation", 664 | "core-foundation-sys", 665 | "libc", 666 | "security-framework-sys", 667 | ] 668 | 669 | [[package]] 670 | name = "security-framework-sys" 671 | version = "2.11.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" 674 | dependencies = [ 675 | "core-foundation-sys", 676 | "libc", 677 | ] 678 | 679 | [[package]] 680 | name = "serde" 681 | version = "1.0.204" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" 684 | dependencies = [ 685 | "serde_derive", 686 | ] 687 | 688 | [[package]] 689 | name = "serde_derive" 690 | version = "1.0.204" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" 693 | dependencies = [ 694 | "proc-macro2", 695 | "quote", 696 | "syn", 697 | ] 698 | 699 | [[package]] 700 | name = "serde_json" 701 | version = "1.0.120" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" 704 | dependencies = [ 705 | "itoa", 706 | "ryu", 707 | "serde", 708 | ] 709 | 710 | [[package]] 711 | name = "sha1" 712 | version = "0.10.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 715 | dependencies = [ 716 | "cfg-if", 717 | "cpufeatures", 718 | "digest", 719 | ] 720 | 721 | [[package]] 722 | name = "slab" 723 | version = "0.4.9" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 726 | dependencies = [ 727 | "autocfg", 728 | ] 729 | 730 | [[package]] 731 | name = "socket2" 732 | version = "0.5.7" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 735 | dependencies = [ 736 | "libc", 737 | "windows-sys 0.52.0", 738 | ] 739 | 740 | [[package]] 741 | name = "syn" 742 | version = "2.0.69" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" 745 | dependencies = [ 746 | "proc-macro2", 747 | "quote", 748 | "unicode-ident", 749 | ] 750 | 751 | [[package]] 752 | name = "tempfile" 753 | version = "3.12.0" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" 756 | dependencies = [ 757 | "cfg-if", 758 | "fastrand", 759 | "once_cell", 760 | "rustix", 761 | "windows-sys 0.59.0", 762 | ] 763 | 764 | [[package]] 765 | name = "thiserror" 766 | version = "1.0.61" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" 769 | dependencies = [ 770 | "thiserror-impl", 771 | ] 772 | 773 | [[package]] 774 | name = "thiserror-impl" 775 | version = "1.0.61" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" 778 | dependencies = [ 779 | "proc-macro2", 780 | "quote", 781 | "syn", 782 | ] 783 | 784 | [[package]] 785 | name = "tokio" 786 | version = "1.38.0" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" 789 | dependencies = [ 790 | "backtrace", 791 | "bytes", 792 | "libc", 793 | "mio", 794 | "num_cpus", 795 | "pin-project-lite", 796 | "socket2", 797 | "tokio-macros", 798 | "windows-sys 0.48.0", 799 | ] 800 | 801 | [[package]] 802 | name = "tokio-macros" 803 | version = "2.3.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" 806 | dependencies = [ 807 | "proc-macro2", 808 | "quote", 809 | "syn", 810 | ] 811 | 812 | [[package]] 813 | name = "tokio-native-tls" 814 | version = "0.3.1" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 817 | dependencies = [ 818 | "native-tls", 819 | "tokio", 820 | ] 821 | 822 | [[package]] 823 | name = "tokio-tungstenite" 824 | version = "0.23.1" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" 827 | dependencies = [ 828 | "futures-util", 829 | "log", 830 | "native-tls", 831 | "tokio", 832 | "tokio-native-tls", 833 | "tungstenite", 834 | ] 835 | 836 | [[package]] 837 | name = "toml" 838 | version = "0.5.11" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 841 | dependencies = [ 842 | "serde", 843 | ] 844 | 845 | [[package]] 846 | name = "tungstenite" 847 | version = "0.23.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" 850 | dependencies = [ 851 | "byteorder", 852 | "bytes", 853 | "data-encoding", 854 | "http", 855 | "httparse", 856 | "log", 857 | "native-tls", 858 | "rand", 859 | "sha1", 860 | "thiserror", 861 | "utf-8", 862 | ] 863 | 864 | [[package]] 865 | name = "typenum" 866 | version = "1.17.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 869 | 870 | [[package]] 871 | name = "unicode-ident" 872 | version = "1.0.12" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 875 | 876 | [[package]] 877 | name = "utf-8" 878 | version = "0.7.6" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 881 | 882 | [[package]] 883 | name = "utf8parse" 884 | version = "0.2.2" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 887 | 888 | [[package]] 889 | name = "vcpkg" 890 | version = "0.2.15" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 893 | 894 | [[package]] 895 | name = "version_check" 896 | version = "0.9.4" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 899 | 900 | [[package]] 901 | name = "wasi" 902 | version = "0.11.0+wasi-snapshot-preview1" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 905 | 906 | [[package]] 907 | name = "windows-sys" 908 | version = "0.48.0" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 911 | dependencies = [ 912 | "windows-targets 0.48.5", 913 | ] 914 | 915 | [[package]] 916 | name = "windows-sys" 917 | version = "0.52.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 920 | dependencies = [ 921 | "windows-targets 0.52.6", 922 | ] 923 | 924 | [[package]] 925 | name = "windows-sys" 926 | version = "0.59.0" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 929 | dependencies = [ 930 | "windows-targets 0.52.6", 931 | ] 932 | 933 | [[package]] 934 | name = "windows-targets" 935 | version = "0.48.5" 936 | source = "registry+https://github.com/rust-lang/crates.io-index" 937 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 938 | dependencies = [ 939 | "windows_aarch64_gnullvm 0.48.5", 940 | "windows_aarch64_msvc 0.48.5", 941 | "windows_i686_gnu 0.48.5", 942 | "windows_i686_msvc 0.48.5", 943 | "windows_x86_64_gnu 0.48.5", 944 | "windows_x86_64_gnullvm 0.48.5", 945 | "windows_x86_64_msvc 0.48.5", 946 | ] 947 | 948 | [[package]] 949 | name = "windows-targets" 950 | version = "0.52.6" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 953 | dependencies = [ 954 | "windows_aarch64_gnullvm 0.52.6", 955 | "windows_aarch64_msvc 0.52.6", 956 | "windows_i686_gnu 0.52.6", 957 | "windows_i686_gnullvm", 958 | "windows_i686_msvc 0.52.6", 959 | "windows_x86_64_gnu 0.52.6", 960 | "windows_x86_64_gnullvm 0.52.6", 961 | "windows_x86_64_msvc 0.52.6", 962 | ] 963 | 964 | [[package]] 965 | name = "windows_aarch64_gnullvm" 966 | version = "0.48.5" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 969 | 970 | [[package]] 971 | name = "windows_aarch64_gnullvm" 972 | version = "0.52.6" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 975 | 976 | [[package]] 977 | name = "windows_aarch64_msvc" 978 | version = "0.48.5" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 981 | 982 | [[package]] 983 | name = "windows_aarch64_msvc" 984 | version = "0.52.6" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 987 | 988 | [[package]] 989 | name = "windows_i686_gnu" 990 | version = "0.48.5" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 993 | 994 | [[package]] 995 | name = "windows_i686_gnu" 996 | version = "0.52.6" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 999 | 1000 | [[package]] 1001 | name = "windows_i686_gnullvm" 1002 | version = "0.52.6" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1005 | 1006 | [[package]] 1007 | name = "windows_i686_msvc" 1008 | version = "0.48.5" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1011 | 1012 | [[package]] 1013 | name = "windows_i686_msvc" 1014 | version = "0.52.6" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1017 | 1018 | [[package]] 1019 | name = "windows_x86_64_gnu" 1020 | version = "0.48.5" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1023 | 1024 | [[package]] 1025 | name = "windows_x86_64_gnu" 1026 | version = "0.52.6" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1029 | 1030 | [[package]] 1031 | name = "windows_x86_64_gnullvm" 1032 | version = "0.48.5" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1035 | 1036 | [[package]] 1037 | name = "windows_x86_64_gnullvm" 1038 | version = "0.52.6" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1041 | 1042 | [[package]] 1043 | name = "windows_x86_64_msvc" 1044 | version = "0.48.5" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1047 | 1048 | [[package]] 1049 | name = "windows_x86_64_msvc" 1050 | version = "0.52.6" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1053 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noteguard" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | forwarder = ["tokio-tungstenite", "tokio", "futures-util"] 8 | 9 | [dependencies] 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | toml = "0.5" 13 | ipnetwork = "0.20.0" 14 | 15 | # forwarder deps 16 | tokio-tungstenite = { version = "0.23.1", optional = true, features = ["native-tls"] } 17 | tokio = { version = "1.38.0", features = ["macros", "time", "sync", "rt-multi-thread"], optional = true } 18 | futures-util = { version = "0.3.30", optional = true } 19 | log = "0.4.22" 20 | env_logger = "0.11.3" 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | tags: 2 | find src -name '*.rs' | xargs ctags 3 | 4 | build-linux: 5 | cargo build --target x86_64-unknown-linux-musl --release 6 | 7 | .PHONY: tags 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noteguard 2 | 3 | A high performance note filter plugin system for [strfry] 4 | 5 | WIP! 6 | 7 | ## Usage 8 | 9 | Filters are registered and loaded from the [noteguard.toml](noteguard.toml) config. 10 | 11 | You can add any new filter you want by implementing the `NoteFilter` trait and registering it with noteguard via the `register_filter` method. 12 | 13 | The `pipeline` config specifies the order in which filters are run. When the first `reject` or `shadowReject` action is hit, then the pipeline stops and returns the rejection error. 14 | 15 | ```toml 16 | pipeline = ["protected_events", "kinds", "whitelist", "ratelimit", "forwarder"] 17 | 18 | [filters.ratelimit] 19 | posts_per_minute = 8 20 | whitelist = ["127.0.0.1"] 21 | message = "rate-limit: you note too much" 22 | 23 | [filters.whitelist] 24 | pubkeys = ["16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93"] 25 | ips = ["127.0.0.1"] 26 | 27 | [filters.kinds] 28 | kinds = [30065, 1064] 29 | 30 | [filters.kinds.messages] 31 | 30065 = "blocked: files on nostr is dumb" 32 | 1064 = "blocked: files on nostr is dumb" 33 | 34 | [filters.protected_events] 35 | 36 | [filters.forwarder] 37 | relay = "ws://localhost:8080" 38 | queue_size = 2000 39 | ``` 40 | 41 | ## Installation 42 | 43 | You can install noteguard by copying the binary to the strfry directory. 44 | 45 | Static musl builds are convenient ways to package noteguard for deployment. It enables you to copy the binary directly to your server, ensure that you are using the correct architecture that your server is running. 46 | 47 | You most likely want `x86_64-unknown-linux-musl` or `aarch64-unknown-linux-musl`. Install this target with rustup, build noteguard, and copy the binary to the server: 48 | 49 | ```sh 50 | $ rustup target add x86_64-unknown-linux-musl 51 | $ cargo build --target x86_64-unknown-linux-musl --release 52 | $ scp ./target/x86_64-unknown-linux-musl/release/noteguard server:strfry 53 | $ scp noteguard.toml server:strfry 54 | ``` 55 | 56 | Test that the binary executes by running it on the server: 57 | 58 | ```sh 59 | $ cd strfry 60 | $ <<<'{}' ./noteguard 61 | Failed to parse input: missing field `type` at line 1 column 2 62 | ``` 63 | 64 | Configure `noteguard.toml` with your preferred filters. 65 | 66 | Now you can then setup your `strfry.conf` to use the noteguard by adding it as a writePolicy plugin: 67 | 68 | ``` 69 | writePolicy { 70 | # If non-empty, path to an executable script that implements the writePolicy plugin logic 71 | plugin = "./noteguard" 72 | } 73 | ``` 74 | 75 | And you're done! Enjoy. 76 | 77 | ## Filters 78 | 79 | You can use any of the builtin filters, or create your own! 80 | 81 | This is the initial release, and only includes one filter so far: 82 | 83 | ### Ratelimit 84 | 85 | * name: `ratelimit` 86 | 87 | The ratelimit filter limits the rate at which notes are written to the relay per-ip. 88 | 89 | Settings: 90 | 91 | - `notes_per_minute`: the number of notes per minute which are allowed to be written per ip. 92 | 93 | - `whitelist` *optional*: a list of IP4 or IP6 addresses that are allowed to bypass the ratelimit. 94 | 95 | - `message` *optional*: the error message to return when connection is rate-limited. default is: `rate-limited: you are noting too much` 96 | 97 | ### Whitelist 98 | 99 | * name: `whitelist` 100 | 101 | The whitelist filter only allows notes to pass if it matches a particular pubkey or source ip: 102 | 103 | - `pubkeys` *optional*: a list of hex public keys to let through 104 | 105 | - `ips` *optional*: a list of ip addresses to let through 106 | 107 | Either criteria can match 108 | 109 | ### Blacklist 110 | 111 | * name: `blacklist` 112 | 113 | The blacklist filter blocks notes that match any pubkey, ip, or CIDR range: 114 | 115 | - `pubkeys` *optional*: a list of hex public keys to block 116 | 117 | - `ips` *optional*: a list of IP addresses to block 118 | 119 | - `cidrs` *optional*: a list of CIDR ranges to block 120 | 121 | ### Kinds 122 | 123 | * name: `kinds` 124 | 125 | A filter that blacklists certain kinds 126 | 127 | - `kinds`: a list of kind integers to block 128 | 129 | - `kinds.messages` *optional*: a map of kinds to message to deliver when the kind is blocked 130 | 131 | Example: 132 | 133 | ```toml 134 | [filters.kinds] 135 | kinds = [30065, 1064] 136 | 137 | [filters.kinds.messages] 138 | 30065 = "blocked: files on nostr is dumb" 139 | 1064 = "blocked: files on nostr is dumb" 140 | ``` 141 | 142 | ### Protected Events 143 | 144 | See [nip70] 145 | 146 | * name: `protected_events` 147 | 148 | There are no config options, but an empty config entry is still needed: 149 | 150 | `[filters.protected_events]` 151 | 152 | ### Forwarder 153 | 154 | * name: `forwarder` 155 | 156 | You need to compile with the `forwarder` feature to enable this filter: 157 | 158 | ```sh 159 | $ cargo build --features forwarder --release 160 | ``` 161 | 162 | The forwarder filter allows you to forward notes to another relay. Notes will 163 | be queued if the connection goes down (up to the `queue_size` buffer limit) 164 | 165 | - `relay` - the relay to forward notes to, eg: `ws://localhost:8080` 166 | 167 | - `queue_size` *optional* - size of the note queue, this is used to buffer notes if the connection goes down. Default is 1000. 168 | 169 | ## Testing 170 | 171 | You can test your filters like so: 172 | 173 | ```sh 174 | $ cargo build 175 | $ >, 10 | pub ips: Option>, 11 | pub cidrs: Option>, 12 | } 13 | 14 | #[derive(Default)] 15 | pub struct Blacklist { 16 | pubkeys: Option>, 17 | ips: Option>, 18 | cidrs: Option>, 19 | } 20 | 21 | impl<'de> Deserialize<'de> for Blacklist { 22 | fn deserialize(deserializer: D) -> Result 23 | where 24 | D: serde::Deserializer<'de>, 25 | { 26 | let config = BlacklistConfig::deserialize(deserializer)?; 27 | Ok(Blacklist { 28 | pubkeys: config.pubkeys, 29 | ips: config.ips, 30 | cidrs: config.cidrs.map(|cidrs| { 31 | cidrs 32 | .into_iter() 33 | .filter_map(|s| IpNetwork::from_str(&s).ok()) 34 | .collect() 35 | }), 36 | }) 37 | } 38 | } 39 | 40 | impl Blacklist { 41 | fn is_ip_blocked(&self, ip: &str) -> bool { 42 | if let Some(ips) = &self.ips { 43 | if ips.contains(&ip.to_string()) { 44 | return true; 45 | } 46 | } 47 | 48 | if let Ok(addr) = IpAddr::from_str(ip) { 49 | if let Some(cidrs) = &self.cidrs { 50 | if cidrs.iter().any(|network| network.contains(addr)) { 51 | return true; 52 | } 53 | } 54 | } 55 | 56 | false 57 | } 58 | } 59 | 60 | impl NoteFilter for Blacklist { 61 | fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage { 62 | let reject_message = "blocked: pubkey/ip is blacklisted".to_string(); 63 | 64 | if let Some(pubkeys) = &self.pubkeys { 65 | if pubkeys.contains(&msg.event.pubkey) { 66 | return OutputMessage::new( 67 | msg.event.id.clone(), 68 | Action::Reject, 69 | Some(reject_message), 70 | ); 71 | } 72 | } 73 | 74 | if self.is_ip_blocked(&msg.source_info) { 75 | return OutputMessage::new(msg.event.id.clone(), Action::Reject, Some(reject_message)); 76 | } 77 | 78 | OutputMessage::new(msg.event.id.clone(), Action::Accept, None) 79 | } 80 | 81 | fn name(&self) -> &'static str { 82 | "blacklist" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/filters/content.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, NoteFilter, OutputMessage}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Default)] 5 | pub struct Content { 6 | filters: Vec, 7 | } 8 | 9 | impl NoteFilter for Content { 10 | fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage { 11 | for filter in &self.filters { 12 | if msg.event.content.contains(filter) { 13 | return OutputMessage::new(msg.event.id.clone(), Action::ShadowReject, None); 14 | } 15 | } 16 | 17 | OutputMessage::new(msg.event.id.clone(), Action::Accept, None) 18 | } 19 | 20 | fn name(&self) -> &'static str { 21 | "content" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/filters/forwarder.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, Note, NoteFilter, OutputMessage}; 2 | use futures_util::{SinkExt, StreamExt}; 3 | use log::{debug, error, info}; 4 | use serde::Deserialize; 5 | use serde_json::json; 6 | use tokio::sync::mpsc::{self, Receiver, Sender}; 7 | use tokio::time::{sleep, timeout, Duration}; 8 | use tokio_tungstenite::connect_async; 9 | use tokio_tungstenite::tungstenite::Message; 10 | use tokio_tungstenite::WebSocketStream; 11 | 12 | #[derive(Default, Deserialize)] 13 | pub struct Forwarder { 14 | relay: String, 15 | 16 | /// the size of our bounded queue 17 | queue_size: Option, 18 | 19 | /// The channel used for communicating with the forwarder thread 20 | #[serde(skip)] 21 | channel: Option>, 22 | } 23 | 24 | async fn client_reconnect( 25 | relay: &str, 26 | ) -> WebSocketStream> { 27 | loop { 28 | match connect_async(relay).await { 29 | Err(e) => { 30 | error!("failed to connect to relay {}: {}", relay, e); 31 | sleep(Duration::from_secs(5)).await; 32 | continue; 33 | } 34 | Ok((ws, _)) => { 35 | info!("connected to relay: {}", relay); 36 | return ws; 37 | } 38 | } 39 | } 40 | } 41 | 42 | async fn forwarder_task(relay: String, mut rx: Receiver) { 43 | let stream = client_reconnect(&relay).await; 44 | let (mut writer, mut reader) = stream.split(); 45 | 46 | loop { 47 | tokio::select! { 48 | result = timeout(Duration::from_secs(10), rx.recv()) => { 49 | match result { 50 | Ok(Some(note)) => { 51 | if let Err(e) = writer.send(Message::Text(serde_json::to_string(&json!(["EVENT", note])).unwrap())).await { 52 | error!("got error: '{}', reconnecting...", e); 53 | let (w, r) = client_reconnect(&relay).await.split(); 54 | writer = w; 55 | reader = r; 56 | } 57 | }, 58 | Ok(None) => { 59 | // Channel has been closed, exit the loop 60 | error!("channel closed, stopping forwarder_task"); 61 | break; 62 | } 63 | Err(_) => { 64 | // Timeout occurred, send a ping 65 | // try reading for pongs, etc 66 | let _r = reader.next(); 67 | debug!("timeout reading note queue, sending ping"); 68 | 69 | if let Err(e) = writer.send(Message::Ping(vec![])).await { 70 | error!("error during ping ({}), reconnecting...", e); 71 | let (w, r) = client_reconnect(&relay).await.split(); 72 | writer = w; 73 | reader = r; 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | impl NoteFilter for Forwarder { 83 | fn name(&self) -> &'static str { 84 | "forwarder" 85 | } 86 | 87 | fn filter_note(&mut self, input: &InputMessage) -> OutputMessage { 88 | if self.channel.is_none() { 89 | let (tx, rx) = mpsc::channel(self.queue_size.unwrap_or(1000) as usize); 90 | let relay = self.relay.clone(); 91 | 92 | tokio::task::spawn(async move { 93 | forwarder_task(relay, rx).await; 94 | }); 95 | 96 | self.channel = Some(tx); 97 | } 98 | 99 | // Add code to process input and send through channel 100 | if let Some(ref channel) = self.channel { 101 | if let Err(e) = channel.try_send(input.event.clone()) { 102 | eprintln!("could not forward note: {}", e); 103 | } 104 | } 105 | 106 | // Create and return an appropriate OutputMessage 107 | OutputMessage::new(input.event.id.clone(), Action::Accept, None) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/filters/kinds.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, NoteFilter, OutputMessage}; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize, Default)] 6 | pub struct Kinds { 7 | kinds: Vec, 8 | messages: Option>, 9 | } 10 | 11 | impl NoteFilter for Kinds { 12 | fn filter_note(&mut self, input: &InputMessage) -> OutputMessage { 13 | let kind = input.event.kind; 14 | if self.kinds.contains(&kind) { 15 | let msg = self 16 | .messages 17 | .as_ref() 18 | .and_then(|msgs| msgs.get(&kind.to_string()).cloned()) 19 | .unwrap_or_else(|| "blocked: note kind is not allowed here".to_string()); 20 | OutputMessage::new(input.event.id.clone(), Action::Reject, Some(msg)) 21 | } else { 22 | OutputMessage::new(input.event.id.clone(), Action::Accept, None) 23 | } 24 | } 25 | 26 | fn name(&self) -> &'static str { 27 | "kinds" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/filters/mod.rs: -------------------------------------------------------------------------------- 1 | mod blacklist; 2 | mod content; 3 | mod kinds; 4 | mod protected_events; 5 | mod ratelimit; 6 | mod whitelist; 7 | 8 | #[cfg(feature = "forwarder")] 9 | mod forwarder; 10 | 11 | pub use blacklist::Blacklist; 12 | pub use content::Content; 13 | pub use kinds::Kinds; 14 | pub use protected_events::ProtectedEvents; 15 | pub use ratelimit::RateLimit; 16 | pub use whitelist::Whitelist; 17 | 18 | #[cfg(feature = "forwarder")] 19 | pub use forwarder::Forwarder; 20 | -------------------------------------------------------------------------------- /src/filters/protected_events.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, NoteFilter, OutputMessage}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Default)] 5 | pub struct ProtectedEvents {} 6 | 7 | impl NoteFilter for ProtectedEvents { 8 | fn filter_note(&mut self, input: &InputMessage) -> OutputMessage { 9 | if let Some(tag) = input.event.tags.first() { 10 | if let Some(entry) = tag.first() { 11 | if entry == "-" { 12 | return OutputMessage::new( 13 | input.event.id.clone(), 14 | Action::Reject, 15 | Some("blocked: event marked as protected".to_string()), 16 | ); 17 | } 18 | } 19 | } 20 | 21 | OutputMessage::new(input.event.id.clone(), Action::Accept, None) 22 | } 23 | 24 | fn name(&self) -> &'static str { 25 | "protected_events" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/filters/ratelimit.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, NoteFilter, OutputMessage}; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | use std::time::{Duration, Instant}; 5 | 6 | pub struct Tokens { 7 | pub tokens: i32, 8 | pub last_post: Instant, 9 | } 10 | 11 | #[derive(Deserialize, Default)] 12 | pub struct RateLimit { 13 | pub posts_per_minute: i32, 14 | pub whitelist: Option>, 15 | pub message: Option, 16 | 17 | #[serde(skip)] 18 | pub sources: HashMap, 19 | } 20 | 21 | impl NoteFilter for RateLimit { 22 | fn name(&self) -> &'static str { 23 | "ratelimit" 24 | } 25 | 26 | fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage { 27 | if let Some(whitelist) = &self.whitelist { 28 | if whitelist.contains(&msg.source_info) { 29 | return OutputMessage::new(msg.event.id.clone(), Action::Accept, None); 30 | } 31 | } 32 | 33 | if !self.sources.contains_key(&msg.source_info) { 34 | self.sources.insert( 35 | msg.source_info.to_owned(), 36 | Tokens { 37 | last_post: Instant::now(), 38 | tokens: self.posts_per_minute, 39 | }, 40 | ); 41 | return OutputMessage::new(msg.event.id.clone(), Action::Accept, None); 42 | } 43 | 44 | let entry = self.sources.get_mut(&msg.source_info).expect("impossiburu"); 45 | let now = Instant::now(); 46 | let mut diff = now - entry.last_post; 47 | 48 | let min = Duration::from_secs(60); 49 | if diff > min { 50 | diff = min; 51 | } 52 | 53 | let percent = (diff.as_secs() as f32) / 60.0; 54 | let new_tokens = (percent * self.posts_per_minute as f32).floor() as i32; 55 | entry.tokens += new_tokens - 1; 56 | 57 | if entry.tokens <= 0 { 58 | entry.tokens = 0; 59 | } 60 | 61 | if entry.tokens >= self.posts_per_minute { 62 | entry.tokens = self.posts_per_minute - 1; 63 | } 64 | 65 | if entry.tokens == 0 { 66 | let message = self 67 | .message 68 | .as_deref() 69 | .unwrap_or("rate-limited: you are noting too much"); 70 | 71 | return OutputMessage::new( 72 | msg.event.id.clone(), 73 | Action::Reject, 74 | Some(message.to_owned()), 75 | ); 76 | } 77 | 78 | entry.last_post = now; 79 | OutputMessage::new(msg.event.id.clone(), Action::Accept, None) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/filters/whitelist.rs: -------------------------------------------------------------------------------- 1 | use crate::{Action, InputMessage, NoteFilter, OutputMessage}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Default)] 5 | pub struct Whitelist { 6 | pub pubkeys: Option>, 7 | pub ips: Option>, 8 | } 9 | 10 | impl NoteFilter for Whitelist { 11 | fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage { 12 | if let Some(pubkeys) = &self.pubkeys { 13 | if pubkeys.contains(&msg.event.pubkey) { 14 | return OutputMessage::new(msg.event.id.clone(), Action::Accept, None); 15 | } 16 | } 17 | 18 | if let Some(ips) = &self.ips { 19 | if ips.contains(&msg.source_info) { 20 | return OutputMessage::new(msg.event.id.clone(), Action::Accept, None); 21 | } 22 | } 23 | 24 | OutputMessage::new( 25 | msg.event.id.clone(), 26 | Action::Reject, 27 | Some("blocked: pubkey/ip not on the whitelist".to_string()), 28 | ) 29 | } 30 | 31 | fn name(&self) -> &'static str { 32 | "whitelist" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod filters; 2 | mod messages; 3 | mod note_filter; 4 | 5 | pub use messages::{Action, InputMessage, OutputMessage}; 6 | pub use note_filter::{Note, NoteFilter}; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use noteguard::filters::{Blacklist, Content, Kinds, ProtectedEvents, RateLimit, Whitelist}; 2 | 3 | #[cfg(feature = "forwarder")] 4 | use noteguard::filters::Forwarder; 5 | 6 | use log::info; 7 | use noteguard::{Action, InputMessage, NoteFilter, OutputMessage}; 8 | use serde::de::DeserializeOwned; 9 | use serde::Deserialize; 10 | use std::collections::HashMap; 11 | use std::io::{self, Read}; 12 | 13 | #[derive(Deserialize)] 14 | struct Config { 15 | pipeline: Vec, 16 | filters: HashMap, 17 | } 18 | 19 | type ConstructFilter = Box Result, toml::de::Error>>; 20 | 21 | #[derive(Default)] 22 | struct Noteguard { 23 | registered_filters: HashMap, 24 | loaded_filters: Vec>, 25 | } 26 | 27 | impl Noteguard { 28 | pub fn new() -> Self { 29 | let mut noteguard = Noteguard::default(); 30 | noteguard.register_builtin_filters(); 31 | noteguard 32 | } 33 | 34 | pub fn register_filter(&mut self) { 35 | self.registered_filters.insert( 36 | F::name(&F::default()).to_string(), 37 | Box::new(|filter_config| { 38 | filter_config 39 | .try_into() 40 | .map(|filter: F| Box::new(filter) as Box) 41 | }), 42 | ); 43 | } 44 | 45 | /// All builtin filters are registered here, and are made available with 46 | /// every new instance of [`Noteguard`] 47 | fn register_builtin_filters(&mut self) { 48 | self.register_filter::(); 49 | self.register_filter::(); 50 | self.register_filter::(); 51 | self.register_filter::(); 52 | self.register_filter::(); 53 | self.register_filter::(); 54 | 55 | #[cfg(feature = "forwarder")] 56 | self.register_filter::(); 57 | } 58 | 59 | /// Run the loaded filters. You must call `load_config` before calling this, otherwise 60 | /// not filters will be run. 61 | fn run(&mut self, input: InputMessage) -> OutputMessage { 62 | let mut mout: Option = None; 63 | 64 | let id = input.event.id.clone(); 65 | for filter in &mut self.loaded_filters { 66 | let out = filter.filter_note(&input); 67 | match out.action { 68 | Action::Accept => { 69 | mout = Some(out); 70 | continue; 71 | } 72 | Action::Reject => { 73 | return out; 74 | } 75 | Action::ShadowReject => { 76 | return out; 77 | } 78 | } 79 | } 80 | 81 | mout.unwrap_or_else(|| OutputMessage::new(id, Action::Accept, None)) 82 | } 83 | 84 | /// Initializes a noteguard config. If it finds any filter configurations 85 | /// matching the registered filters, it loads those into our filter pipeline. 86 | fn load_config(&mut self, config: &Config) -> Result<(), toml::de::Error> { 87 | self.loaded_filters.clear(); 88 | 89 | for name in &config.pipeline { 90 | let config_value = config 91 | .filters 92 | .get(name) 93 | .unwrap_or_else(|| panic!("could not find filter configuration for {}", name)); 94 | 95 | if let Some(constructor) = self.registered_filters.get(name.as_str()) { 96 | let filter = constructor(config_value.clone())?; 97 | self.loaded_filters.push(filter); 98 | } else { 99 | panic!("Found config settings with no matching filter: {}", name); 100 | } 101 | } 102 | 103 | Ok(()) 104 | } 105 | } 106 | 107 | #[cfg(feature = "forwarder")] 108 | #[tokio::main] 109 | async fn main() { 110 | noteguard(); 111 | } 112 | 113 | #[cfg(not(feature = "forwarder"))] 114 | fn main() { 115 | noteguard(); 116 | } 117 | 118 | fn serialize_output_message(msg: &OutputMessage) -> String { 119 | serde_json::to_string(msg).expect("OutputMessage should always serialize correctly") 120 | } 121 | 122 | fn noteguard() { 123 | env_logger::init(); 124 | info!("running noteguard"); 125 | 126 | let config_path = "noteguard.toml"; 127 | let mut noteguard = Noteguard::new(); 128 | 129 | let config: Config = { 130 | let mut file = std::fs::File::open(config_path).expect("Failed to open config file"); 131 | let mut contents = String::new(); 132 | file.read_to_string(&mut contents) 133 | .expect("Failed to read config file"); 134 | toml::from_str(&contents).expect("Failed to parse config file") 135 | }; 136 | 137 | noteguard 138 | .load_config(&config) 139 | .expect("Expected filter config to be loaded ok"); 140 | 141 | let stdin = io::stdin(); 142 | 143 | for line in stdin.lines() { 144 | let line = match line { 145 | Ok(line) => line, 146 | Err(e) => { 147 | eprintln!("Failed to get line: {}", e); 148 | continue; 149 | } 150 | }; 151 | 152 | let input_message: InputMessage = match serde_json::from_str(&line) { 153 | Ok(msg) => msg, 154 | Err(e) => { 155 | eprintln!("Failed to parse input: {}", e); 156 | continue; 157 | } 158 | }; 159 | 160 | if input_message.message_type != "new" { 161 | let out = OutputMessage::new( 162 | input_message.event.id.clone(), 163 | Action::Reject, 164 | Some("invalid strfry write policy input".to_string()), 165 | ); 166 | println!("{}", serialize_output_message(&out)); 167 | continue; 168 | } 169 | 170 | let out = noteguard.run(input_message); 171 | let json = serialize_output_message(&out); 172 | 173 | println!("{}", json); 174 | } 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use super::*; 180 | use noteguard::{Action, Note}; 181 | 182 | // Helper function to create a mock InputMessage 183 | fn create_mock_input_message( 184 | event_id: &str, 185 | message_type: &str, 186 | source_info: &str, 187 | ) -> InputMessage { 188 | InputMessage { 189 | message_type: message_type.to_string(), 190 | event: Note { 191 | id: event_id.to_string(), 192 | pubkey: "mock_pubkey".to_string(), 193 | created_at: 0, 194 | kind: 1, 195 | tags: vec![vec!["-".to_string()]], 196 | content: "mock_content".to_string(), 197 | sig: "mock_signature".to_string(), 198 | }, 199 | received_at: 0, 200 | source_type: "mock_source".to_string(), 201 | source_info: source_info.to_string(), 202 | } 203 | } 204 | 205 | #[test] 206 | fn test_register_builtin_filters() { 207 | let noteguard = Noteguard::new(); 208 | assert!(noteguard.registered_filters.contains_key("ratelimit")); 209 | assert!(noteguard.registered_filters.contains_key("whitelist")); 210 | assert!(noteguard.registered_filters.contains_key("blacklist")); 211 | assert!(noteguard 212 | .registered_filters 213 | .contains_key("protected_events")); 214 | assert!(noteguard.registered_filters.contains_key("kinds")); 215 | } 216 | 217 | #[test] 218 | fn test_load_config() { 219 | let mut noteguard = Noteguard::new(); 220 | 221 | // Create a mock config with one filter (RateLimit) 222 | let config: Config = toml::from_str( 223 | r#" 224 | pipeline = ["ratelimit"] 225 | 226 | [filters.ratelimit] 227 | posts_per_minute = 3 228 | "#, 229 | ) 230 | .expect("Failed to parse config"); 231 | 232 | assert!(noteguard.load_config(&config).is_ok()); 233 | assert_eq!(noteguard.loaded_filters.len(), 1); 234 | } 235 | 236 | #[test] 237 | fn test_run_filters_accept() { 238 | let mut noteguard = Noteguard::new(); 239 | 240 | // Create a mock config with one filter (RateLimit) 241 | let config: Config = toml::from_str( 242 | r#" 243 | pipeline = ["ratelimit"] 244 | 245 | [filters.ratelimit] 246 | posts_per_minute = 3 247 | "#, 248 | ) 249 | .expect("Failed to parse config"); 250 | 251 | noteguard 252 | .load_config(&config) 253 | .expect("Failed to load config"); 254 | 255 | let input_message = create_mock_input_message("test_event_1", "new", "mock_source_info"); 256 | let output_message = noteguard.run(input_message); 257 | 258 | assert_eq!(output_message.action, Action::Accept); 259 | } 260 | 261 | #[test] 262 | fn test_run_filters_shadow_reject() { 263 | let mut noteguard = Noteguard::new(); 264 | 265 | // Create a mock config with one filter (ProtectedEvents) which will shadow reject the input 266 | let config: Config = toml::from_str( 267 | r#" 268 | pipeline = ["protected_events"] 269 | 270 | [filters.protected_events] 271 | "#, 272 | ) 273 | .expect("Failed to parse config"); 274 | 275 | noteguard 276 | .load_config(&config) 277 | .expect("Failed to load config"); 278 | 279 | let input_message = create_mock_input_message("test_event_3", "new", "mock_source_info"); 280 | let output_message = noteguard.run(input_message); 281 | 282 | assert_eq!(output_message.action, Action::Reject); 283 | } 284 | 285 | #[test] 286 | fn test_whitelist_reject() { 287 | let mut noteguard = Noteguard::new(); 288 | 289 | // Create a mock config with one filter (Whitelist) which will reject the input 290 | let config: Config = toml::from_str( 291 | r#" 292 | pipeline = ["whitelist"] 293 | [filters.whitelist] 294 | pubkeys = ["something"] 295 | "#, 296 | ) 297 | .expect("Failed to parse config"); 298 | 299 | noteguard 300 | .load_config(&config) 301 | .expect("Failed to load config"); 302 | 303 | let input_message = create_mock_input_message("test_event_2", "new", "mock_source_info"); 304 | let output_message = noteguard.run(input_message); 305 | 306 | assert_eq!(output_message.action, Action::Reject); 307 | } 308 | 309 | #[test] 310 | fn test_blacklist_reject() { 311 | let mut noteguard = Noteguard::new(); 312 | 313 | let config: Config = toml::from_str( 314 | r#" 315 | pipeline = ["blacklist"] 316 | [filters.blacklist] 317 | pubkeys = ["mock_pubkey"] 318 | "#, 319 | ) 320 | .expect("Failed to parse config"); 321 | 322 | noteguard 323 | .load_config(&config) 324 | .expect("Failed to load config"); 325 | 326 | let input_message = create_mock_input_message("test_event_3", "new", "mock_source_info"); 327 | let output_message = noteguard.run(input_message); 328 | 329 | assert_eq!(output_message.action, Action::Reject); 330 | assert_eq!( 331 | output_message.msg.expect("Failed to get message"), 332 | "blocked: pubkey/ip is blacklisted".to_string() 333 | ); 334 | } 335 | 336 | #[test] 337 | fn test_blacklist_accept() { 338 | let mut noteguard = Noteguard::new(); 339 | 340 | let config: Config = toml::from_str( 341 | r#" 342 | pipeline = ["blacklist"] 343 | [filters.blacklist] 344 | pubkeys = ["not_blacklisted"] 345 | "#, 346 | ) 347 | .expect("Failed to parse config"); 348 | 349 | noteguard 350 | .load_config(&config) 351 | .expect("Failed to load config"); 352 | 353 | let input_message = create_mock_input_message("test_event_4", "new", "mock_source_info"); 354 | let output_message = noteguard.run(input_message); 355 | 356 | assert_eq!(output_message.action, Action::Accept); 357 | } 358 | 359 | #[test] 360 | fn test_blacklist_cidr() { 361 | let mut noteguard = Noteguard::new(); 362 | 363 | let config: Config = toml::from_str( 364 | r#" 365 | pipeline = ["blacklist"] 366 | [filters.blacklist] 367 | cidrs = ["127.0.0.1/24"] 368 | "#, 369 | ) 370 | .expect("Failed to parse config"); 371 | 372 | noteguard 373 | .load_config(&config) 374 | .expect("Failed to load config"); 375 | 376 | let test_cases = [ 377 | ("127.0.0.1", true), 378 | ("127.0.0.2", true), 379 | ("128.0.0.1", false), 380 | ("127.1.0.1", false), 381 | ("127.0.1.1", false), 382 | ]; 383 | 384 | for (ip, should_reject) in test_cases.iter() { 385 | let input_message = create_mock_input_message("event_id", "new", ip); 386 | let output_message = noteguard.run(input_message); 387 | 388 | if *should_reject { 389 | assert_eq!(output_message.action, Action::Reject); 390 | } else { 391 | assert_eq!(output_message.action, Action::Accept); 392 | } 393 | } 394 | } 395 | 396 | #[test] 397 | fn test_deserialize_input_message() { 398 | let input_json = r#" 399 | { 400 | "type": "new", 401 | "event": { 402 | "id": "test_event_5", 403 | "pubkey": "mock_pubkey", 404 | "created_at": 0, 405 | "kind": 1, 406 | "tags": [], 407 | "content": "mock_content", 408 | "sig": "mock_signature" 409 | }, 410 | "receivedAt": 0, 411 | "sourceType": "mock_source", 412 | "sourceInfo": "mock_source_info" 413 | } 414 | "#; 415 | 416 | let input_message: InputMessage = 417 | serde_json::from_str(input_json).expect("Failed to deserialize input message"); 418 | assert_eq!(input_message.event.id, "test_event_5"); 419 | assert_eq!(input_message.message_type, "new"); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | use crate::Note; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize)] 5 | pub struct InputMessage { 6 | #[serde(rename = "type")] 7 | pub message_type: String, 8 | pub event: Note, 9 | #[serde(rename = "receivedAt")] 10 | pub received_at: u64, 11 | #[serde(rename = "sourceType")] 12 | pub source_type: String, 13 | #[serde(rename = "sourceInfo")] 14 | pub source_info: String, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] 18 | #[serde(rename_all = "camelCase")] 19 | pub enum Action { 20 | Accept, 21 | Reject, 22 | ShadowReject, 23 | } 24 | 25 | #[derive(Serialize)] 26 | pub struct OutputMessage { 27 | pub id: String, 28 | pub action: Action, 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub msg: Option, 31 | } 32 | 33 | impl OutputMessage { 34 | pub fn new(id: String, action: Action, msg: Option) -> Self { 35 | OutputMessage { id, action, msg } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/note_filter.rs: -------------------------------------------------------------------------------- 1 | use crate::{InputMessage, OutputMessage}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Deserialize, Serialize, Clone)] 5 | pub struct Note { 6 | pub id: String, 7 | pub pubkey: String, 8 | pub content: String, 9 | pub created_at: i64, 10 | pub kind: i64, 11 | pub tags: Vec>, 12 | pub sig: String, 13 | } 14 | 15 | pub trait NoteFilter { 16 | fn filter_note(&mut self, msg: &InputMessage) -> OutputMessage; 17 | 18 | /// A key corresponding to an entry in the noteguard.toml file. 19 | fn name(&self) -> &'static str; 20 | } 21 | -------------------------------------------------------------------------------- /test/delayed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | while true 4 | do 5 | echo '{"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}}' 6 | 7 | sleep ${1:-0.1} 8 | done 9 | 10 | -------------------------------------------------------------------------------- /test/delayed-nostril: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | while true 4 | do 5 | note="$(nostril --silent --content hello)" 6 | echo "{\"type\": \"new\",\"receivedAt\":12345,\"sourceType\":\"IP4\",\"sourceInfo\": \"127.0.0.2\",\"event\":$note}" 7 | 8 | sleep ${1:-0.1} 9 | done 10 | 11 | -------------------------------------------------------------------------------- /test/inputs: -------------------------------------------------------------------------------- 1 | {"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"}} 2 | {"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"}} 3 | {"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"}} 4 | {"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"}} 5 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 6 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 7 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.1","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 8 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 9 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 10 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 11 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 12 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 13 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 14 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 15 | {"type": "new","receivedAt":12345,"sourceType":"IP4","sourceInfo": "127.0.0.2","event":{"id": "68421a122cef086512b2c5bd29ca6285ced8bd8e302e347e3c5d90466c860a76","pubkey": "16c21558762108afc34e4ff19e4ed51d9a48f79e0c34531efc423d21ab435e93","created_at": 1720408658,"kind": 1,"tags": [],"content": "hi","sig": "7b76471744ded2b720ca832cdc89e670f6093ce38aeef55a5c6a4e077883d7d80dda1e9051032fb1faa1c3c212c517e93ee42b3ceac8e8e9b04bad46a361de90"}} 16 | --------------------------------------------------------------------------------