diff --git a/.cargo/config.toml b/.cargo/config.toml index ee12bf2..4fa60c5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,8 @@ [env] RELAY_ADDRESSES_CSV = "ws://localhost" REPORTINATOR_SECRET = "feef9c2dcd6a1175a97dfbde700fa54f58ce69d4f30963f70efcc7257636759f" -SLACK_SIGNING_SECRET = "something" RUST_LOG = "info" +SLACK_CHANNEL_ID="C06SBEF40G0" +SLACK_SIGNING_SECRET = "something" +SLACK_TOKEN = "something" TEMPLATES_DIR= "templates" diff --git a/Cargo.lock b/Cargo.lock index 92f5846..4bf17ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -136,9 +136,9 @@ checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "flate2", "futures-core", @@ -155,7 +155,7 @@ checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -177,18 +177,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -288,7 +288,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "itoa", "matchit", @@ -301,7 +301,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.0", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" [[package]] name = "cfg-if" @@ -539,15 +539,15 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -561,6 +561,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "colorchoice" version = "1.0.0" @@ -657,8 +684,8 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.58", + "strsim 0.10.0", + "syn 2.0.60", ] [[package]] @@ -669,7 +696,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -679,7 +706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -687,9 +714,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deranged" @@ -720,15 +747,15 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -774,15 +801,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" dependencies = [ "crc32fast", "miniz_oxide", @@ -885,7 +912,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -935,9 +962,9 @@ dependencies = [ [[package]] name = "gcloud-sdk" -version = "0.24.5" +version = "0.24.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b576eba42739c388264de61bd3fb96c186071266116b960fb2d84e594be8175f" +checksum = "fa9b9d27ae1188efa150d35e7dfdb9caeb40c06016c8ae1b0ac4ee6bbe95d5dc" dependencies = [ "async-trait", "chrono", @@ -1023,9 +1050,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -1062,9 +1089,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1203,14 +1230,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.3", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -1231,7 +1258,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.28", - "rustls 0.21.10", + "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", ] @@ -1244,10 +1271,10 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "log", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-native-certs 0.7.0", "rustls-pki-types", "tokio", @@ -1275,7 +1302,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "native-tls", "tokio", @@ -1294,7 +1321,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.2.0", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -1360,7 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -1439,9 +1466,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "linux-raw-sys" @@ -1456,16 +1483,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02c042191c2e3f27147decfad8182eea2c7dd1c6c1733562e25d3d401369669d" dependencies = [ "bech32", - "reqwest 0.12.3", + "reqwest 0.12.4", "serde", "serde_json", ] [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1483,7 +1510,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1525,7 +1552,7 @@ checksum = "5d58e362dc7206e9456ddbcdbd53c71ba441020e62104703075a69151e38d85f" dependencies = [ "base64 0.22.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-tls", "hyper-util", "indexmap 2.2.6", @@ -1546,7 +1573,7 @@ checksum = "8b07a5eb561b8cbc16be2d216faf7757f9baf3bfb94dbb0fae3df8387a5bb47f" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "metrics", "num_cpus", "quanta", @@ -1643,7 +1670,7 @@ dependencies = [ "js-sys", "negentropy", "once_cell", - "reqwest 0.12.3", + "reqwest 0.12.4", "scrypt", "serde", "serde_json", @@ -1842,7 +1869,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -1871,9 +1898,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -1881,15 +1908,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -1960,7 +1987,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2021,7 +2048,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2083,9 +2110,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -2110,7 +2137,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2139,17 +2166,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "ractor" -version = "0.9.7" -source = "git+https://github.com/slawlor/ractor?rev=26a01d69e1d83a6d6f1cfcda668acb6deee74b13#26a01d69e1d83a6d6f1cfcda668acb6deee74b13" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d551077e2f2f354bd9cbe5520fe493cf57618bc70c1de4609f022ee9cc5d33" dependencies = [ "async-trait", "dashmap", @@ -2201,11 +2229,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] @@ -2258,12 +2286,15 @@ version = "0.1.0" dependencies = [ "anyhow", "axum 0.7.5", + "clap", "env_logger", "futures", "gcloud-sdk", "handlebars", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", + "hyper-rustls 0.26.0", + "hyper-util", "libc", "log", "metrics", @@ -2272,7 +2303,7 @@ dependencies = [ "pretty_assertions", "ractor", "regex", - "reqwest 0.12.3", + "reqwest 0.12.4", "serde", "serde_json", "slack-morphism", @@ -2310,7 +2341,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.10", + "rustls 0.21.12", "rustls-native-certs 0.6.3", "rustls-pemfile 1.0.4", "serde", @@ -2332,20 +2363,20 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "base64 0.22.0", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.3", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-rustls 0.26.0", "hyper-tls", "hyper-util", @@ -2357,7 +2388,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", @@ -2421,9 +2452,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -2434,9 +2465,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -2446,14 +2477,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -2504,9 +2535,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" @@ -2520,9 +2551,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -2693,14 +2724,14 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "indexmap 2.2.6", "itoa", @@ -2732,11 +2763,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", "chrono", "hex", "indexmap 1.9.3", @@ -2750,14 +2781,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +checksum = "c8b3a576c4eb2924262d5951a3b737ccaf16c931e39a2810c36f9a7e25575557" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -2804,9 +2835,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -2852,9 +2883,9 @@ dependencies = [ [[package]] name = "slack-morphism" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146a31c2e89296c311240661e21f13c8d6e33523ac9e8f96d55fc1f8dadf899" +checksum = "8c42ccefce572e3c2f8c934316ea051401cd0aad2a0691a042e0c0dad488ae1f" dependencies = [ "async-recursion", "async-trait", @@ -2869,7 +2900,7 @@ dependencies = [ "hex", "http 1.1.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-rustls 0.26.0", "hyper-util", "lazy_static", @@ -2920,6 +2951,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -2939,9 +2976,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -2956,9 +2993,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "system-configuration" @@ -2995,22 +3032,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3025,9 +3062,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -3046,9 +3083,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", @@ -3107,7 +3144,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3126,7 +3163,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.10", + "rustls 0.21.12", "tokio", ] @@ -3136,7 +3173,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -3172,7 +3209,7 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-native-certs 0.7.0", "rustls-pki-types", "tokio", @@ -3191,7 +3228,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pin-project-lite", "tokio", "tracing", @@ -3310,7 +3347,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3371,7 +3408,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "sha1", "thiserror", @@ -3515,7 +3552,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", "wasm-bindgen-shared", ] @@ -3549,7 +3586,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3637,7 +3674,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3655,7 +3692,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -3675,17 +3712,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3696,9 +3734,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3708,9 +3746,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3720,9 +3758,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3732,9 +3776,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3744,9 +3788,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3756,9 +3800,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3768,9 +3812,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winreg" @@ -3815,7 +3859,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] [[package]] @@ -3835,5 +3879,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.60", ] diff --git a/Cargo.toml b/Cargo.toml index 5c3bda5..e00c37f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,23 +9,26 @@ default-run = "reportinator_server" [dependencies] anyhow = "1.0.82" axum = "0.7.5" +clap = "4.5.4" env_logger = "0.11.3" futures = "0.3.30" -gcloud-sdk = { version = "0.24.5", features = ["google-pubsub-v1"] } +gcloud-sdk = { version = "0.24.6", features = ["google-pubsub-v1"] } handlebars = "5.1.2" http-body-util = "0.1.1" -hyper = "1.2.0" -libc = "0.2.153" +hyper = { version = "1.3.1", features = ["http2", "http1"] } +hyper-rustls = "0.26" +hyper-util = { version = "0.1.3", features = ["http2", "http1", "full"] } +libc = "0.2.154" log = "0.4.21" metrics = "0.22.3" metrics-exporter-prometheus = "0.14.0" nostr-sdk = "0.30.0" -ractor = {git = "https://github.com/slawlor/ractor", rev = "26a01d69e1d83a6d6f1cfcda668acb6deee74b13" } +ractor = "0.9.8" regex = "1.10.4" -reqwest = "0.12.3" +reqwest = "0.12.4" serde = { version = "1.0.200", features = ["derive"] } -serde_json = "1.0.115" -slack-morphism = { version = "2.1.0", features = ["axum"] } +serde_json = "1.0.116" +slack-morphism = { version = "2.2.0", features = ["axum"] } tokio = { version = "1.37.0", features = ["full"] } tokio-util = { version = "0.7.10", features = ["rt"] } tower = "0.4.13" diff --git a/README.md b/README.md index 1636783..403b42e 100644 --- a/README.md +++ b/README.md @@ -11,52 +11,53 @@ The server employs the actor model via [`ractor`](https://github.com/slawlor/rac **System Architecture Diagram:** ``` -┌────────────────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ -│ ┌───────────────────────┐ │ OpenAI │ Cleanstr │ │ Manual Moderation │ -│ │wss://relay.nos.social │◀─┼────────Report ────────│(Google Cloud Function)│──Not flagged────▶│ Slack Channel │ -│ └────────────────────▲──┘ │ Event └───────────────────────┘ └──────────────────────┘ -│ │ │ ▲ │ -│ Nostr Network │ │ │ │ -│ │ │ ┌────────────────┐ │ -│ ┌─────────────┐ │ │ │ nostr-events │ │ -│ │Encrypted DM │ │ │ │ Pubsub Topic │ │ -│ └─────────────┘ │ │ └────────────────┘ │ -│ │ │ │ ▲ │ -└─────────────┼────────┼─────┘ ┌────────────┼──────────────────────────────────────────┼───────────────┐ - │ │ │ ┌──────────┴──────────┐ │ │ - │ │ │ │ ┌─────────────────┐ │ │ │ - │ │ │ │ │ GooglePublisher │ │ │ │ - │ │ │ │ └─────────────────┘ │ │ │ - Gift │ │ │ EventEnqueuer │ │ │ - Wrapped │ │ └─────────────────────┘ │ │ - DM with │ │ ▲ Report │ - Report │ │ │ Request │ - Request Manual │ ┌────────────────────┐ │ │ - │ Report │ │ GiftUnwrapper │ │ │ - │ Event │ └────────────────────┘ │ │ - │ │ │ ▲ │ │ - │ │ │ │ │ │ - │ │ │┌──────────────────────┐ ┌──────────▼────────┐ │ - │ │ ││┌────────────────────┐│ │ ┌────────────────┐│ │ - │ └────────────────────────────┼┼┤ NostrService ││ Manual │ │ Slack endpoint ││ │ - └─────────────────────────────────────┼▶│ ││◀─────Label─────────┼─│ ││ │ - ││└────────────────────┘│ │ └────────────────┘│ │ - ││ RelayEventDispatcher │ │ Axum HTTP server │ │ - │└──────────────────────┘ └───────────────────┘ │ - │ │ - │ │ - │ Reportinator Server │ - └───────────────────────────────────────────────────────────────────────┘ + ┌────────────────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐ + │ ┌───────────────────────┐ │ OpenAI │ Cleanstr │ Not flagged │ Manual Moderation │ + │ │wss://relay.nos.social │◀─┼───Report ────│(Google Cloud Function)│───by OpenAI─────▶│ Slack Channel │ + │ └────────────────────▲──┘ │ Event └───────────────────────┘ └────▲─────────────────┘ + │ │ │ ▲ │ │ + │ Nostr Network │ │ │ │ │ + │ │ │ ┌────────────────┐ │ │ + │ ┌─────────────┐ │ │ │ nostr-events │ │ │ + │ │Encrypted DM │ │ │ │ Pubsub Topic │ │ │ + │ └─────────────┘ │ │ └────────────────┘ │ │ + │ │ │ │ ▲ │ │ + └─────────────┼────────┼─────┘ │ │ │ + │ │ ┌────────────┼───────────────────────────────────┼──────┼──────────┐ + │ │ │ ┌──────────┴──────────┐ ┌──────────────────┴──┐ │ │ + │ │ │ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │ │ + │ │ │ │ │ GooglePublisher │ │ │ │ SlackClient │ │ │ │ + Gift │ │ │ └─────────────────┘ │ │ └─────────────────┘ │ │ │ + Wrapped │ │ │ EventEnqueuer │ │ SlackWriter │ │ │ + DM with │ │ └─────────────────────┘ └─────────────────────┘ Report │ + Report Manual │ ▲ ▲ Request │ + Request Report │ │ │ │ │ + │ Event │ note │ │ │ + │ │ │ report │ │ │ + │ │ │ request │ │ │ + │ │ │ │ │ │ │ + │ │ │ ┌────────────────────┐ npub │ │ │ + │ │ │ │ GiftUnwrapper │───report ───────┘ │ │ + │ │ │ └────────────────────┘ request │ │ + │ │ │ ▲ │ │ + │ │ │ │ │ │ + │ │ │┌──────────────────────┐ ┌──────────▼────────┐ │ + │ │ ││┌────────────────────┐│ │ ┌────────────────┐│ │ + │ └───────────────────┼┼┤ NostrService ││ Manual │ │ Slack endpoint ││ │ + └────────────────────────────┼▶│ ││◀─────Label─────────┼─│ ││ │ + ││└────────────────────┘│ │ └────────────────┘│ │ + ││ RelayEventDispatcher │ │ Axum HTTP server │ │ + │└──────────────────────┘ └───────────────────┘ │ + │ Reportinator Server │ + └──────────────────────────────────────────────────────────────────┘ ``` The `NostrService` listens for direct messages sent to the Reportinator account, which contain requests for the moderation of specific Nostr notes. These requests are then forwarded to the `GiftUnwrapper` for initial processing in accordance with the NIP-17 standard. -After processing, the `GiftUnwrapper` sends the validated and extracted messages as moderation requests to the `EventEnqueuer`. This component utilizes the `GooglePublisher` to publish the reports to a designated Google PubSub topic, intended for moderation request analysis on nos.social. +After processing, the `GiftUnwrapper` sends the validated and extracted messages as moderation requests to the `EventEnqueuer`. This component utilizes the `GooglePublisher` to publish the reports to a designated Google PubSub topic, intended for moderation request analysis on nos.social. Npub report requests skip the google topic and are posted directly to the Slack channel It's noteworthy that this PubSub topic also consolidates moderation requests from various sources, thereby positioning this server as one among several entry points. -A [Google Cloud Function](https://github.com/planetary-social/cleanstr) linked to this topic employs AI to analyze these reports. Reports deemed suspicious are either directly published to `wss://relay.nos.social` if automatically flagged, or forwarded to a Slack channel for manual review. If flagged during the manual review, they are anonymously published through the Reportinator account back into the Nostr network. - - +A [Google Cloud Function](https://github.com/planetary-social/cleanstr) linked to this topic employs AI to analyze these reports. Reports are either directly published to `wss://relay.nos.social` if automatically flagged, or forwarded to a Slack channel for manual review. If flagged during the manual review, they are anonymously published through the Reportinator account back into the Nostr network. ## Setup @@ -70,7 +71,7 @@ Ensure these environment variables are set before running the Reportinator Serve ### Running Locally 1. **Local Nostr Relay**: Start a Nostr relay at `ws://localhost`. - + 2. **Google Cloud Access**: Authenticate with Google Cloud: ```sh gcloud auth application-default login diff --git a/src/actors.rs b/src/actors.rs index 18a18c3..79aed4f 100644 --- a/src/actors.rs +++ b/src/actors.rs @@ -7,6 +7,9 @@ pub use gift_unwrapper::GiftUnwrapper; pub mod event_enqueuer; pub use event_enqueuer::{EventEnqueuer, PubsubPort}; +pub mod slack_writer; +pub use slack_writer::{SlackClientPort, SlackClientPortBuilder, SlackWriter}; + pub mod supervisor; pub use supervisor::Supervisor; diff --git a/src/actors/event_enqueuer.rs b/src/actors/event_enqueuer.rs index a9b68ee..ff16ad2 100644 --- a/src/actors/event_enqueuer.rs +++ b/src/actors/event_enqueuer.rs @@ -1,5 +1,5 @@ -use crate::actors::messages::EventEnqueuerMessage; use crate::domain_objects::ReportRequest; +use crate::{actors::messages::EventEnqueuerMessage, domain_objects::ReportTarget}; use anyhow::Result; use metrics::counter; use ractor::{Actor, ActorProcessingErr, ActorRef}; @@ -52,6 +52,11 @@ where ) -> Result<(), ActorProcessingErr> { match message { EventEnqueuerMessage::Enqueue(report_request) => { + if let ReportTarget::Pubkey(_) = report_request.target() { + info!("Ignoring pubkey report request for event enqueuer, these go directly to slack"); + return Ok(()); + } + if let Err(e) = state.pubsub_publisher.publish_event(&report_request).await { counter!("events_enqueued_error").increment(1); error!("Failed to publish event: {}", e); @@ -59,10 +64,7 @@ where } counter!("events_enqueued").increment(1); - info!( - "Event {} enqueued for moderation", - report_request.reported_event().id() - ); + info!("Event {} enqueued for moderation", report_request.target()); } } diff --git a/src/actors/gift_unwrapper.rs b/src/actors/gift_unwrapper.rs index 11610de..301a750 100644 --- a/src/actors/gift_unwrapper.rs +++ b/src/actors/gift_unwrapper.rs @@ -40,8 +40,22 @@ impl Actor for GiftUnwrapper { state: &mut Self::State, ) -> Result<(), ActorProcessingErr> { match message { - // Decrypts and forwards private messages to the output port. - GiftUnwrapperMessage::UnwrapEvent(gift_wrap) => { + // Decrypts and forwards private messages so they can be sent to + // google pubsub or whatever is hooked to the output port. + // + // Note that this is a good example of what we are trying to achieve + // in terms of separation of concerns, keeping the actor logic just + // as an orchestrator for our domain code. The brains of the + // operation are in the domain model. + GiftUnwrapperMessage::UnwrapEvent(maybe_gift_wrap) => { + // 1) The actor's message handling, which includes the message From + // implementation, deal with massaging the message to gather the + // input for... + let Some(gift_wrap) = maybe_gift_wrap else { + return Ok(()); + }; + + // 2) ...the domain model, which does the real work. let report_request = match gift_wrap.extract_report_request(&state.keys) { Ok(report_request) => report_request, Err(e) => { @@ -50,10 +64,13 @@ impl Actor for GiftUnwrapper { } }; + // 3) Resulting model output is used to create events + // that are sent to the output port for the next actor or any other + // IO needed info!( - "Request from {} to moderate event {}", + "Request from {} to moderate {}", report_request.reporter_pubkey(), - report_request.reported_event().id() + report_request.target() ); state.message_parsed_output_port.send(report_request) @@ -80,7 +97,7 @@ mod tests { use tokio::time::{sleep, Duration}; #[tokio::test] - async fn test_gift_unwrapper() { + async fn test_gift_unwrapper_with_event() { // Fake of course let reportinator_secret = "feef9c2dcd6a1175a97dfbde700fa54f58ce69d4f30963f70efcc7257636759f"; @@ -128,10 +145,77 @@ mod tests { cast!( parser_actor_ref, - GiftUnwrapperMessage::UnwrapEvent(gift_wrapped_event) + GiftUnwrapperMessage::UnwrapEvent(Some(gift_wrapped_event)) + ) + .unwrap(); + + // This happens when during the From conversion, the event + cast!(parser_actor_ref, GiftUnwrapperMessage::UnwrapEvent(None)).unwrap(); + + tokio::spawn(async move { + sleep(Duration::from_secs(1)).await; + parser_actor_ref.stop(None); + receiver_actor_ref.stop(None); + }); + + parser_handle.await.unwrap(); + receiver_actor_handle.await.unwrap(); + + assert_eq!(messages_received.lock().await.as_ref(), [report_request]); + } + + #[tokio::test] + async fn test_gift_unwrapper_with_pubkey() { + // Fake of course + let reportinator_secret = + "feef9c2dcd6a1175a97dfbde700fa54f58ce69d4f30963f70efcc7257636759f"; + let reportinator_keys = Keys::parse(reportinator_secret).unwrap(); + let receiver_pubkey = reportinator_keys.public_key(); + + let sender_secret = "51ce70ac70753e62f9baf4a8ce5e1334c30360ab14783016775ecb42dc322571"; + let sender_keys = Keys::parse(sender_secret).unwrap(); + + let bad_guy_keys = Keys::generate(); + + let report_request_string = json!({ + "reportedPubkey": bad_guy_keys.public_key().to_string(), + "reporterPubkey": sender_keys.public_key().to_string(), + "reporterText": "This is hateful. Report it!" + }) + .to_string(); + let report_request: ReportRequest = serde_json::from_str(&report_request_string).unwrap(); + + let gift_wrapped_event = report_request + .as_gift_wrap(&sender_keys, &receiver_pubkey) + .await + .unwrap(); + + let messages_received = Arc::new(Mutex::new(Vec::::new())); + let (receiver_actor_ref, receiver_actor_handle) = + Actor::spawn(None, TestActor::default(), Some(messages_received.clone())) + .await + .unwrap(); + + let (parser_actor_ref, parser_handle) = + Actor::spawn(None, GiftUnwrapper, reportinator_keys) + .await + .unwrap(); + + cast!( + parser_actor_ref, + GiftUnwrapperMessage::SubscribeToEventUnwrapped(Box::new(receiver_actor_ref.clone())) + ) + .unwrap(); + + cast!( + parser_actor_ref, + GiftUnwrapperMessage::UnwrapEvent(Some(gift_wrapped_event)) ) .unwrap(); + // This happens when during the From conversion, the event + cast!(parser_actor_ref, GiftUnwrapperMessage::UnwrapEvent(None)).unwrap(); + tokio::spawn(async move { sleep(Duration::from_secs(1)).await; parser_actor_ref.stop(None); diff --git a/src/actors/messages.rs b/src/actors/messages.rs index effbf5a..3cf194e 100644 --- a/src/actors/messages.rs +++ b/src/actors/messages.rs @@ -1,7 +1,9 @@ use crate::domain_objects::*; +use metrics::counter; use nostr_sdk::prelude::*; use ractor::{port::OutputPortSubscriber, RpcReplyPort}; use std::fmt::Debug; +use tracing::error; pub enum SupervisorMessage { Publish(ModeratedReport), @@ -11,21 +13,31 @@ pub enum SupervisorMessage { pub enum RelayEventDispatcherMessage { Connect, Reconnect, - SubscribeToEventReceived(OutputPortSubscriber), + SubscribeToEventReceived(OutputPortSubscriber), EventReceived(Event), Publish(ModeratedReport), GetNip05(PublicKey, RpcReplyPort>), } pub enum GiftUnwrapperMessage { - UnwrapEvent(GiftWrappedReportRequest), + // If an event couldn't be mapped to a GiftWrappedReportRequest, it will be None + UnwrapEvent(Option), SubscribeToEventUnwrapped(OutputPortSubscriber), } // How to subscribe to actors that publish DM messages like RelayEventDispatcher -impl From for GiftUnwrapperMessage { - fn from(gift_wrap: GiftWrappedReportRequest) -> Self { - GiftUnwrapperMessage::UnwrapEvent(gift_wrap) +impl From for GiftUnwrapperMessage { + fn from(event: Event) -> Self { + let gift_wrapped_report_request = match GiftWrappedReportRequest::try_from(event) { + Ok(gift) => Some(gift), + Err(e) => { + counter!("event_received_error").increment(1); + error!("Failed to get gift wrap event: {}", e); + None + } + }; + + GiftUnwrapperMessage::UnwrapEvent(gift_wrapped_report_request) } } @@ -41,6 +53,17 @@ impl From for EventEnqueuerMessage { } } +#[derive(Debug)] +pub enum SlackWriterMessage { + Write(ReportRequest), +} + +impl From for SlackWriterMessage { + fn from(report_request: ReportRequest) -> Self { + SlackWriterMessage::Write(report_request) + } +} + #[derive(Debug, Clone)] pub enum TestActorMessage { EventHappened(T), diff --git a/src/actors/relay_event_dispatcher.rs b/src/actors/relay_event_dispatcher.rs index e24cfa2..ce612a3 100644 --- a/src/actors/relay_event_dispatcher.rs +++ b/src/actors/relay_event_dispatcher.rs @@ -1,5 +1,4 @@ use crate::actors::messages::RelayEventDispatcherMessage; -use crate::domain_objects::GiftWrappedReportRequest; use crate::service_manager::ServiceManager; use anyhow::Result; use metrics::counter; @@ -20,7 +19,7 @@ impl Default for RelayEventDispatcher { } } pub struct State { - event_received_output_port: OutputPort, + event_received_output_port: OutputPort, subscription_task_manager: Option, nostr_client: T, } @@ -153,18 +152,7 @@ impl Actor for RelayEventDispatcher { subscriber.subscribe_to_port(&state.event_received_output_port); } RelayEventDispatcherMessage::EventReceived(event) => { - let gift_wrapped_report_request = match GiftWrappedReportRequest::try_from(event) { - Ok(gift) => gift, - Err(e) => { - counter!("event_received_error").increment(1); - error!("Failed to get gift wrap event: {}", e); - return Ok(()); - } - }; - - state - .event_received_output_port - .send(gift_wrapped_report_request); + state.event_received_output_port.send(event); counter!("event_received").increment(1); } RelayEventDispatcherMessage::Publish(moderated_report) => { @@ -314,7 +302,7 @@ mod tests { .await .unwrap(); - let received_messages = Arc::new(Mutex::new(Vec::::new())); + let received_messages = Arc::new(Mutex::new(Vec::::new())); let (receiver_ref, receiver_handle) = Actor::spawn(None, TestActor::default(), Some(received_messages.clone())) @@ -343,10 +331,7 @@ mod tests { assert_eq!( received_messages.lock().await.as_ref(), - [ - GiftWrappedReportRequest::try_from(first_event).unwrap(), - GiftWrappedReportRequest::try_from(second_event).unwrap() - ] + [first_event, second_event] ); } } diff --git a/src/actors/slack_writer.rs b/src/actors/slack_writer.rs new file mode 100644 index 0000000..d8405bf --- /dev/null +++ b/src/actors/slack_writer.rs @@ -0,0 +1,159 @@ +use super::messages::SupervisorMessage; +use crate::actors::messages::SlackWriterMessage; +use crate::domain_objects::{ReportRequest, ReportTarget}; +use anyhow::Result; +use metrics::counter; +use ractor::{Actor, ActorProcessingErr, ActorRef}; +use tracing::{error, info}; + +pub struct SlackWriter { + _phantom: std::marker::PhantomData, +} + +impl Default for SlackWriter { + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +pub struct State { + slack_client: T, +} + +#[ractor::async_trait] +impl Actor for SlackWriter +where + T: SlackClientPort + Send + Sync + Sized + 'static, +{ + type Msg = SlackWriterMessage; + type State = State; + type Arguments = T; + + async fn pre_start( + &self, + _: ActorRef, + slack_client: T, + ) -> Result { + let state = State { slack_client }; + + Ok(state) + } + + async fn handle( + &self, + _: ActorRef, + message: Self::Msg, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + match message { + Self::Msg::Write(report_request) => { + if let ReportTarget::Event(_) = report_request.target() { + info!("Ignoring event report request for slack writer"); + return Ok(()); + } + + info!( + "Sending report request {} to slack", + report_request.target() + ); + if let Err(e) = state.slack_client.write_message(&report_request).await { + counter!("slack_write_message_error").increment(1); + error!("Failed to write slack message: {}", e); + return Ok(()); + } + + counter!("slack_write_message").increment(1); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use nostr_sdk::prelude::Keys; + use ractor::cast; + use serde_json::json; + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::Mutex; + + #[derive(Clone)] + struct TestSlackClient { + requests_sent_to_slack: Arc>>, + } + impl TestSlackClient { + fn new() -> Self { + Self { + requests_sent_to_slack: Arc::new(Mutex::new(Vec::new())), + } + } + } + + #[ractor::async_trait] + impl SlackClientPort for TestSlackClient { + async fn write_message(&self, report_request: &ReportRequest) -> Result<()> { + self.requests_sent_to_slack + .lock() + .await + .push(report_request.clone()); + Ok(()) + } + } + + use super::*; + #[tokio::test] + async fn test_slack_writer() { + let test_slack_client = TestSlackClient::new(); + + let (slack_writer_ref, slack_writer_handle) = + Actor::spawn(None, SlackWriter::default(), test_slack_client.clone()) + .await + .unwrap(); + + let pubkey_to_report = Keys::generate().public_key(); + + let report_request_string = json!({ + "reportedPubkey": pubkey_to_report.to_string(), + "reporterPubkey": Keys::generate().public_key().to_string(), + "reporterText": "This is hateful. Report it!" + }) + .to_string(); + + let report_request: ReportRequest = serde_json::from_str(&report_request_string).unwrap(); + + cast!( + slack_writer_ref, + SlackWriterMessage::Write(report_request.clone()) + ) + .unwrap(); + + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + slack_writer_ref.stop(None); + }); + + slack_writer_handle.await.unwrap(); + + assert_eq!( + test_slack_client + .requests_sent_to_slack + .lock() + .await + .as_ref(), + [report_request] + ); + } +} + +pub trait SlackClientPortBuilder: Send + Sync + 'static { + fn build(&self, nostr_actor: ActorRef) -> Result; +} + +#[ractor::async_trait] +pub trait SlackClientPort: Send + Sync + 'static { + async fn write_message(&self, report_request: &ReportRequest) -> Result<()>; +} diff --git a/src/actors/supervisor.rs b/src/actors/supervisor.rs index 66e0eae..c8960b2 100644 --- a/src/actors/supervisor.rs +++ b/src/actors/supervisor.rs @@ -1,7 +1,7 @@ -use crate::actors::PubsubPort; use crate::actors::{ messages::{GiftUnwrapperMessage, RelayEventDispatcherMessage, SupervisorMessage}, - EventEnqueuer, GiftUnwrapper, NostrPort, RelayEventDispatcher, + EventEnqueuer, GiftUnwrapper, NostrPort, PubsubPort, RelayEventDispatcher, + SlackClientPortBuilder, SlackWriter, }; use anyhow::Result; use metrics::counter; @@ -9,13 +9,14 @@ use nostr_sdk::prelude::*; use ractor::{call_t, cast, Actor, ActorProcessingErr, ActorRef, SupervisionEvent}; use tracing::error; -pub struct Supervisor { - _phantom: std::marker::PhantomData<(T, U)>, +pub struct Supervisor { + _phantom: std::marker::PhantomData<(T, U, V)>, } -impl Default for Supervisor +impl Default for Supervisor where T: NostrPort, U: PubsubPort, + V: SlackClientPortBuilder, { fn default() -> Self { Self { @@ -25,19 +26,25 @@ where } #[ractor::async_trait] -impl Actor for Supervisor +impl Actor for Supervisor where T: NostrPort, U: PubsubPort, + V: SlackClientPortBuilder, { type Msg = SupervisorMessage; type State = ActorRef; - type Arguments = (T, U, Keys); + type Arguments = (T, U, V, Keys); async fn pre_start( &self, myself: ActorRef, - (nostr_subscriber, google_publisher, reportinator_keys): (T, U, Keys), + (nostr_subscriber, google_publisher, slack_writer_builder, reportinator_keys): ( + T, + U, + V, + Keys, + ), ) -> Result { // Spawn actors and wire them together let (event_dispatcher, _event_dispatcher_handle) = Actor::spawn_linked( @@ -69,11 +76,26 @@ where ) .await?; + let slack_client_port = slack_writer_builder.build(myself.clone())?; + + let (slack_writer, _slack_writer_handle) = Actor::spawn_linked( + Some("slack_writer".to_string()), + SlackWriter::default(), + slack_client_port, + myself.get_cell(), + ) + .await?; + cast!( gift_unwrapper, GiftUnwrapperMessage::SubscribeToEventUnwrapped(Box::new(event_enqueuer)) )?; + cast!( + gift_unwrapper, + GiftUnwrapperMessage::SubscribeToEventUnwrapped(Box::new(slack_writer)) + )?; + // Connect as the last message once everything is wired up cast!(event_dispatcher, RelayEventDispatcherMessage::Connect)?; diff --git a/src/adapters.rs b/src/adapters.rs index 2348e97..9c45249 100644 --- a/src/adapters.rs +++ b/src/adapters.rs @@ -4,3 +4,5 @@ pub mod http_server; pub use http_server::HttpServer; pub mod nostr_service; pub use nostr_service::NostrService; +pub mod slack_client_adapter; +pub use slack_client_adapter::SlackClientAdapterBuilder; diff --git a/src/adapters/google_publisher.rs b/src/adapters/google_publisher.rs index f72de60..43b3ce7 100644 --- a/src/adapters/google_publisher.rs +++ b/src/adapters/google_publisher.rs @@ -1,6 +1,6 @@ use crate::actors::PubsubPort; -use crate::domain_objects::ReportRequest; -use anyhow::{Context, Result}; +use crate::domain_objects::{ReportRequest, ReportTarget}; +use anyhow::{bail, Context, Result}; use gcloud_sdk::{ google::pubsub::v1::{publisher_client::PublisherClient, PublishRequest, PubsubMessage}, *, @@ -35,6 +35,9 @@ impl GooglePublisher { #[ractor::async_trait] impl PubsubPort for GooglePublisher { async fn publish_event(&mut self, report_request: &ReportRequest) -> Result<()> { + if let ReportTarget::Pubkey(_) = report_request.target() { + bail!("Cannot publish event with Pubkey target to Google Pubsub") + } let pubsub_message = PubsubMessage { data: serde_json::to_vec(report_request) .context("Failed to serialize event to JSON")?, diff --git a/src/adapters/http_server/router.rs b/src/adapters/http_server/router.rs index 3a35c88..3418129 100644 --- a/src/adapters/http_server/router.rs +++ b/src/adapters/http_server/router.rs @@ -69,6 +69,11 @@ fn setup_metrics() -> Result String { + let target_message = match report_request.target() { + ReportTarget::Event(event) => format!( + r#" + *Reported Pubkey:* {} + *Reported Event Id:* `{}` + *Reported Event content:* + ``` + {} + ``` + "#, + reported_nip05_markdown, event.id, event.content + ), + ReportTarget::Pubkey(_) => format!( + r#" + *Reported Pubkey:* {} + "#, + reported_nip05_markdown + ), + }; + let message = format!( r#" 🚩 *New Moderation Report* 🚩 @@ -131,26 +151,20 @@ fn slack_processed_message( *Report Id:* `{}` *Requested By*: {} - *Reason:* + *Reporter Reason:* + ``` {} ``` - *Reported Pubkey:* {} - *Reported Event Id:* `{}` - *Reported Event content:* - ``` {} - ``` "#, slack_username, category, report_id, reporter_nip05_markdown, report_request.reporter_text().unwrap_or(&"".to_string()), - reported_nip05_markdown, - report_request.reported_event().id, - report_request.reported_event().content + target_message, ); let trimmed_string = message @@ -168,6 +182,26 @@ fn slack_skipped_message( report_request: ReportRequest, reported_nip05_markdown: String, ) -> String { + let target_message = match report_request.target() { + ReportTarget::Event(event) => format!( + r#" + *Reported Pubkey:* {} + *Reported Event Id:* `{}` + *Reported Event content:* + ``` + {} + ``` + "#, + reported_nip05_markdown, event.id, event.content + ), + ReportTarget::Pubkey(_) => format!( + r#" + *Reported Pubkey:* {} + "#, + reported_nip05_markdown + ), + }; + let message = format!( r#" ⏭️ *Moderation Report Skipped* ⏭️ @@ -175,24 +209,17 @@ fn slack_skipped_message( *Report Skipped By:* {} *Requested By*: {} - *Reason:* + *Reporter Reason:* ``` {} ``` - *Reported Pubkey:* {} - *Reported Event Id:* `{}` - *Reported Event content:* - ``` {} - ``` "#, slack_username, reporter_nip05_markdown, report_request.reporter_text().unwrap_or(&"".to_string()), - reported_nip05_markdown, - report_request.reported_event().id, - report_request.reported_event().content + target_message, ); let trimmed_string = message @@ -242,15 +269,33 @@ fn parse_slack_action( .ok_or_else(|| anyhow!("Missing action_id"))?; let reported_event_value = find_block_id(&event_value, "reportedEvent")?; + let reported_pubkey = find_block_id(&event_value, "reportedPubkey")?; let reporter_text = find_block_id(&event_value, "reporterText")?; - let reported_event = Event::from_json(reported_event_value) - .map_err(|_| AppError::slack_parsing_error("reported_event"))?; + let target = match reported_event_value { + None => match reported_pubkey { + None => { + return Err(AppError::slack_parsing_error( + "neither reportedEvent nor reportedPubkey present", + )); + } + Some(reported_pubkey_value) => { + let reported_pubkey = PublicKey::from_hex(reported_pubkey_value) + .map_err(|_| AppError::slack_parsing_error("reported_pubkey"))?; + ReportTarget::Pubkey(reported_pubkey) + } + }, + Some(reported_event_value) => { + let reported_event = Event::from_json(reported_event_value) + .map_err(|_| AppError::slack_parsing_error("reported_event"))?; + ReportTarget::Event(reported_event) + } + }; let reporter_pubkey = PublicKey::from_hex(action_value) .map_err(|_| AppError::slack_parsing_error("reporter_pubkey"))?; - let report_request = ReportRequest::new(reported_event, reporter_pubkey, Some(reporter_text)); + let report_request = ReportRequest::new(target, reporter_pubkey, reporter_text); let maybe_category = ModerationCategory::from_str(action_id).ok(); Ok(( @@ -261,25 +306,32 @@ fn parse_slack_action( )) } -fn find_block_id(event_value: &Value, block_id_text: &str) -> Result { +fn find_block_id(event_value: &Value, block_id_text: &str) -> Result, AppError> { let reported_event_value = event_value["message"]["blocks"] .as_array() .and_then(|blocks| { blocks.iter().find_map(|block| { block["block_id"].as_str().and_then(|block_id| { if block_id == block_id_text { - block["elements"].as_array()?.first()?["elements"] - .as_array()? - .first()?["text"] - .as_str() + let first_element = block["elements"].as_array()?.first()?; + + let maybe_nested = first_element["elements"] + .as_array() + .and_then(|a| a.first()) + .and_then(|v| v["text"].as_str()); + + match maybe_nested { + Some(nested) => Some(nested.to_string()), + None => first_element["text"].as_str().map(|s| s.to_string()), + } } else { None } }) }) - }) - .ok_or_else(|| anyhow!("Missing block_id with value {}", block_id_text))?; - Ok(reported_event_value.to_string()) + }); + + Ok(reported_event_value.map(|s| s.to_string())) } async fn send_slack_response(response_url: &str, response_text: &str) -> Result<()> { @@ -390,7 +442,7 @@ mod tests { ); assert_eq!(username, "daniel"); assert!(maybe_moderated_report.is_some()); - assert_eq!(parsed_report_request.reported_event(), &reported_event); + assert_eq!(parsed_report_request.target(), &reported_event.into()); assert_eq!(parsed_report_request.reporter_pubkey(), &reporter_pubkey); assert_eq!( parsed_report_request.reporter_text(), @@ -426,7 +478,7 @@ mod tests { ); assert_eq!(username, "daniel"); assert!(maybe_moderated_report.is_none()); - assert_eq!(parsed_report_request.reported_event(), &reported_event); + assert_eq!(parsed_report_request.target(), &reported_event.into()); assert_eq!(parsed_report_request.reporter_pubkey(), &reporter_pubkey); assert_eq!( parsed_report_request.reporter_text(), diff --git a/src/adapters/slack_client_adapter.rs b/src/adapters/slack_client_adapter.rs new file mode 100644 index 0000000..3e58012 --- /dev/null +++ b/src/adapters/slack_client_adapter.rs @@ -0,0 +1,203 @@ +use crate::actors::messages::SupervisorMessage; +use crate::actors::{SlackClientPort, SlackClientPortBuilder}; +use crate::domain_objects::{ModerationCategory, ReportRequest}; +use anyhow::Result; +use hyper_rustls::HttpsConnector; +use hyper_util::client::legacy::connect::HttpConnector; +use nostr_sdk::prelude::PublicKey; +use ractor::{call_t, ActorRef}; +use slack_morphism::prelude::*; +use std::env; +use tracing::info; + +#[derive(Clone)] +pub struct SlackClientAdapter { + client: SlackClient>>, + nostr_actor: ActorRef, +} + +#[derive(Default)] +pub struct SlackClientAdapterBuilder {} + +impl SlackClientPortBuilder for SlackClientAdapterBuilder { + fn build(&self, nostr_actor: ActorRef) -> Result { + let client = SlackClient::new(SlackClientHyperConnector::new()?); + Ok(SlackClientAdapter { + client, + nostr_actor, + }) + } +} + +impl SlackClientAdapter { + async fn post_message(&self, message: SlackApiChatPostMessageRequest) -> Result<()> { + let slack_token = env::var("SLACK_TOKEN")?; + let token: SlackApiToken = SlackApiToken::new(slack_token.into()); + let session = self.client.open_session(&token); + + let post_chat_resp = session.chat_post_message(&message).await; + info!("post chat resp: {:#?}", &post_chat_resp); + + Ok(()) + } + + async fn try_njump(&self, pubkey: PublicKey) -> Result { + let maybe_reporter_nip05 = + call_t!(self.nostr_actor, SupervisorMessage::GetNip05, 50, pubkey)?; + + Ok(maybe_reporter_nip05 + .as_ref() + .map(|nip05| format!("https://njump.me/{}", nip05)) + .unwrap_or(format!("`{}`", pubkey))) + } +} + +#[ractor::async_trait] +impl SlackClientPort for SlackClientAdapter { + async fn write_message(&self, report_request: &ReportRequest) -> Result<()> { + let reported_pubkey_or_nip05_link = + match self.try_njump(report_request.target().pubkey()).await { + Ok(link) => link, + Err(e) => { + info!("Failed to get nip05 link: {}", e); + format!("`{}`", report_request.target().pubkey()) + } + }; + + let reporter_pubkey_or_nip05_link = + match self.try_njump(*report_request.reporter_pubkey()).await { + Ok(link) => link, + Err(e) => { + info!("Failed to get nip05 link: {}", e); + format!("`{}`", report_request.target().pubkey()) + } + }; + + let message = PubkeyReportRequestMessage::new( + report_request, + reported_pubkey_or_nip05_link, + reporter_pubkey_or_nip05_link, + ); + + let channel_id = env::var("SLACK_CHANNEL_ID")?; + let message_req = + SlackApiChatPostMessageRequest::new(channel_id.into(), message.render_template()); + + self.post_message(message_req).await + } +} + +#[derive(Debug, Clone)] +pub struct PubkeyReportRequestMessage<'a> { + report_request: &'a ReportRequest, + reported_pubkey_or_nip05_link: String, + reporter_pubkey_or_nip05_link: String, +} +impl<'a> PubkeyReportRequestMessage<'a> { + pub fn new( + report_request: &'a ReportRequest, + reported_pubkey_or_nip05_link: String, + reporter_pubkey_or_nip05_link: String, + ) -> Self { + Self { + report_request, + reported_pubkey_or_nip05_link, + reporter_pubkey_or_nip05_link, + } + } + + fn category_buttons(&self) -> Vec { + let pubkey = self.report_request.reporter_pubkey().to_string(); + + slack_blocks![ + some_into( + SlackBlockButtonElement::new("skip".into(), pt!("Skip")) + .with_style("danger".to_string()) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::Hate).with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::HateThreatening) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::Harassment) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::HarassmentThreatening) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::SelfHarm) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::SelfHarmIntent) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::SelfHarmInstructions) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::Sexual) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::SexualMinors) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::Violence) + .with_value(pubkey.clone()) + ), + some_into( + SlackBlockButtonElement::from(ModerationCategory::ViolenceGraphic) + .with_value(pubkey.clone()) + ) + ] + } +} + +impl<'a> SlackMessageTemplate for PubkeyReportRequestMessage<'a> { + fn render_template(&self) -> SlackMessageContent { + let text = self + .report_request + .reporter_text() + .map(|t| t.to_string()) + .unwrap_or_default(); + + SlackMessageContent::new() + .with_text(format!( + "New moderation request sent by {} to report account {}", + self.reporter_pubkey_or_nip05_link, self.reported_pubkey_or_nip05_link + )) + .with_blocks(slack_blocks![ + some_into(SlackSectionBlock::new().with_text(md!( + "New moderation request sent by {} to report account {}", + self.reporter_pubkey_or_nip05_link, + self.reported_pubkey_or_nip05_link + ))), + some_into(SlackSectionBlock::new().with_text(md!(text))), + some_into( + SlackContextBlock::new(slack_blocks![some(pt!(self + .report_request + .target() + .pubkey() + .to_string()))]) + .with_block_id("reportedPubkey".to_string().into()) + ), + some_into(SlackDividerBlock::new()), + some_into(SlackActionsBlock::new(self.category_buttons())) + ]) + } +} + +impl From for SlackBlockButtonElement { + fn from(category: ModerationCategory) -> Self { + SlackBlockButtonElement::new(category.to_string().into(), pt!(category.to_string())) + } +} diff --git a/src/bin/giftwrapper.rs b/src/bin/giftwrapper.rs index 92b2184..ad8c18b 100644 --- a/src/bin/giftwrapper.rs +++ b/src/bin/giftwrapper.rs @@ -1,40 +1,49 @@ use anyhow::Result; +use clap::{Arg, Command}; use nostr_sdk::prelude::*; -use reportinator_server::{AsGiftWrap, ReportRequest}; -use std::env; +use reportinator_server::{AsGiftWrap, ReportRequest, ReportTarget}; use std::io::{self, BufRead}; use std::str::FromStr; #[tokio::main] async fn main() -> Result<()> { - let args: Vec = env::args().collect(); - - if args.len() < 2 { - eprintln!("Usage: {} ", args[0]); - eprintln!("Example:"); - eprintln!( - r#"echo "I'm a boring comment, someone may report it because it's too boring" | ./target/debug/giftwrapper add5190be4673768546c18b565da3a699241f0e06a75e2dbc03f18663d1b7b27 | nak event ws://localhost"# - ); - - std::process::exit(1); - } - - let receiver_pubkey = PublicKey::from_str(&args[1]).expect("Failed to parse the public key"); - - let stdin = io::stdin(); - let mut iterator = stdin.lock().lines(); - let message = iterator - .next() - .expect("Failed to read message from stdin") - .expect("Failed to read line"); - - // A safe test account with pubkey 817eaa4193d9a571c70b445c2c9b4ec0df306952f9784dfbd6eac680b19b2693 + let matches = Command::new("GiftWrapper") + .version("1.0") + .author("Your Name. ") + .about("Handles sending secret messages using Nostr") + .arg(Arg::new("receiver_pubkey").required(true)) + .arg(Arg::new("reported_pubkey").required(false)) + .get_matches(); + + let receiver_pubkey_str = matches.get_one::("receiver_pubkey").unwrap(); + let receiver_pubkey = + PublicKey::from_str(receiver_pubkey_str).expect("Failed to parse the public key"); + let maybe_reported_pubkey_str = matches.get_one::("reported_pubkey"); let test_secret = "7786a6328328930d6da0d494524dc3a8597abd8f41616621fabb7ad60c9ef143"; let sender_keys = Keys::parse(test_secret).expect("Failed to parse the secret"); - let reported_event = EventBuilder::text_note(&message, []).to_event(&sender_keys)?; + + let target = match maybe_reported_pubkey_str { + Some(reported_pubkey_str) => { + let reported_pubkey = + PublicKey::from_str(reported_pubkey_str).expect("Failed to parse the public key"); + ReportTarget::Pubkey(reported_pubkey) + } + None => { + let stdin = io::stdin(); + let mut iterator = stdin.lock().lines(); + let message = iterator + .next() + .expect("Failed to read message from stdin") + .expect("Failed to read line"); + + let reported_event = EventBuilder::text_note(message, []).to_event(&sender_keys)?; + ReportTarget::Event(reported_event) + } + }; + let reporter_pubkey = sender_keys.public_key(); let reporter_text = Some("This is wrong, report it!".to_string()); - let report_request = ReportRequest::new(reported_event, reporter_pubkey, reporter_text); + let report_request = ReportRequest::new(target, reporter_pubkey, reporter_text); let event_result = report_request .as_gift_wrap(&sender_keys, &receiver_pubkey) .await; diff --git a/src/domain_objects.rs b/src/domain_objects.rs index 85bb673..6f97f40 100644 --- a/src/domain_objects.rs +++ b/src/domain_objects.rs @@ -3,6 +3,7 @@ pub use gift_wrap::GiftWrappedReportRequest; pub mod report_request; pub use report_request::ReportRequest; +pub use report_request::ReportTarget; pub mod as_gift_wrap; diff --git a/src/domain_objects/as_gift_wrap.rs b/src/domain_objects/as_gift_wrap.rs index e16e423..41018ec 100644 --- a/src/domain_objects/as_gift_wrap.rs +++ b/src/domain_objects/as_gift_wrap.rs @@ -5,6 +5,7 @@ use nostr_sdk::prelude::*; #[async_trait] pub trait AsGiftWrap { + #[allow(unused)] async fn as_gift_wrap( &self, reporter_keys: &Keys, @@ -14,8 +15,7 @@ pub trait AsGiftWrap { fn random_time_in_last_two_days(&self) -> Timestamp { let now = Timestamp::now(); let two_days = 2 * 24 * 60 * 60; - let random_time = now - (rand::random::() % two_days); - random_time + now - (rand::random::() % two_days) } } @@ -69,10 +69,11 @@ mod tests { async fn test_as_gift_wrap() { let reporter_keys = Keys::generate(); let receiver_keys = Keys::generate(); - let rumor = EventBuilder::text_note("Hello", []) + let event_to_report = EventBuilder::text_note("Hello", []) .to_event(&reporter_keys) .unwrap(); - let report_request = ReportRequest::new(rumor, reporter_keys.public_key(), None); + let report_request = + ReportRequest::new(event_to_report.into(), reporter_keys.public_key(), None); let gift_wrap = report_request .as_gift_wrap(&reporter_keys, &receiver_keys.public_key()) diff --git a/src/domain_objects/moderated_report.rs b/src/domain_objects/moderated_report.rs index 43d738d..1c5f3c0 100644 --- a/src/domain_objects/moderated_report.rs +++ b/src/domain_objects/moderated_report.rs @@ -1,4 +1,4 @@ -use crate::domain_objects::{ModerationCategory, ReportRequest}; +use crate::domain_objects::{ModerationCategory, ReportRequest, ReportTarget}; use anyhow::Result; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -20,9 +20,11 @@ impl ModeratedReport { return Err(anyhow::anyhow!("REPORTINATOR_SECRET env variable not set")); }; let reportinator_keys = Keys::parse(reportinator_secret)?; - let reported_pubkey = reported_request.reported_event().pubkey; - let reported_event_id = reported_request.reported_event().id; - let tags = Self::set_tags(reported_pubkey, Some(reported_event_id), category); + let (reported_pubkey, reported_event_id) = match reported_request.target() { + ReportTarget::Event(event) => (event.pubkey, Some(event.id)), + ReportTarget::Pubkey(pubkey) => (*pubkey, None), + }; + let tags = Self::set_tags(reported_pubkey, reported_event_id, category); let report_event = EventBuilder::new(Kind::Reporting, category.description(), tags) .to_event(&reportinator_keys)?; diff --git a/src/domain_objects/report_request.rs b/src/domain_objects/report_request.rs index 668fdff..c32620e 100644 --- a/src/domain_objects/report_request.rs +++ b/src/domain_objects/report_request.rs @@ -5,10 +5,50 @@ use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::fmt::{self, Display, Formatter}; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ReportTarget { + #[serde(rename = "reportedEvent")] + Event(Event), + #[serde(rename = "reportedPubkey")] + Pubkey(PublicKey), +} + +impl ReportTarget { + pub fn pubkey(&self) -> PublicKey { + match self { + ReportTarget::Event(event) => event.author(), + ReportTarget::Pubkey(pubkey) => *pubkey, + } + } +} + +impl From for ReportTarget { + fn from(event: Event) -> Self { + ReportTarget::Event(event) + } +} + +impl From for ReportTarget { + fn from(pubkey: PublicKey) -> Self { + ReportTarget::Pubkey(pubkey) + } +} + +impl Display for ReportTarget { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ReportTarget::Event(event) => write!(f, "Event {}", event.id), + ReportTarget::Pubkey(pubkey) => write!(f, "Pubkey {}", pubkey), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReportRequestRumorContent { - reported_event: Event, + #[serde(flatten)] + target: ReportTarget, reporter_text: Option, } @@ -22,14 +62,15 @@ impl ReportRequestRumorContent { impl ReportRequestRumorContent { pub fn into_report_request(self, pubkey: PublicKey) -> ReportRequest { - ReportRequest::new(self.reported_event, pubkey, self.reporter_text) + ReportRequest::new(self.target, pubkey, self.reporter_text) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ReportRequest { - reported_event: Event, + #[serde(flatten)] + target: ReportTarget, reporter_pubkey: PublicKey, reporter_text: Option, } @@ -37,19 +78,19 @@ pub struct ReportRequest { impl ReportRequest { #[allow(unused)] pub fn new( - reported_event: Event, + target: ReportTarget, reporter_pubkey: PublicKey, reporter_text: Option, ) -> Self { ReportRequest { - reported_event, + target, reporter_pubkey, reporter_text, } } - pub fn reported_event(&self) -> &Event { - &self.reported_event + pub fn target(&self) -> &ReportTarget { + &self.target } pub fn reporter_pubkey(&self) -> &PublicKey { @@ -62,7 +103,10 @@ impl ReportRequest { } pub fn valid(&self) -> bool { - self.reported_event.verify().is_ok() + match &self.target { + ReportTarget::Event(event) => event.verify().is_ok(), + ReportTarget::Pubkey(_) => true, + } } pub fn report( @@ -91,7 +135,9 @@ mod tests { use serde_json::json; use std::str::FromStr; - fn setup_test_environment() -> (ReportRequest, Event, PublicKey, Option) { + fn setup_test_environment( + event_target: bool, + ) -> (ReportRequest, ReportTarget, PublicKey, Option) { let reported_secret = "a39b6f282044c4812c1729a783f32d974ed13072632f08201f52d083593d6e76"; let reported_keys = Keys::parse(reported_secret).unwrap(); @@ -99,20 +145,25 @@ mod tests { let reporter_keys = Keys::parse(reporter_secret).unwrap(); let reporter_pubkey = reporter_keys.public_key(); - let reported_event = EventBuilder::text_note("I'm a hateful text", []) - .to_event(&reported_keys) - .unwrap(); + let reported_target = if event_target { + let reported_event = EventBuilder::text_note("I'm a hateful text", []) + .to_event(&reported_keys) + .unwrap(); + ReportTarget::Event(reported_event) + } else { + ReportTarget::Pubkey(reported_keys.public_key()) + }; let reporter_text = Some("This is hateful. Report it!".to_string()); let report_request = ReportRequest::new( - reported_event.clone(), + reported_target.clone(), reporter_pubkey, reporter_text.clone(), ); ( report_request, - reported_event, + reported_target, reporter_pubkey, reporter_text, ) @@ -120,10 +171,10 @@ mod tests { #[test] fn test_report_request() { - let (report_request, reported_event, reporter_pubkey, reporter_text) = - setup_test_environment(); + let (report_request, reported_target, reporter_pubkey, reporter_text) = + setup_test_environment(true); - assert_eq!(report_request.reported_event(), &reported_event); + assert_eq!(report_request.target(), &reported_target); assert_eq!(report_request.reporter_pubkey(), &reporter_pubkey); assert_eq!(report_request.reporter_text(), reporter_text.as_ref()); assert_eq!(report_request.valid(), true); @@ -132,8 +183,8 @@ mod tests { #[test] fn test_report_event() { - let (report_request, reported_event, _reporter_pubkey, _reporter_text) = - setup_test_environment(); + let (report_request, reported_target, _reporter_pubkey, _reporter_text) = + setup_test_environment(true); let category = ModerationCategory::from_str("hate").unwrap(); let maybe_report_event = report_request.report(Some(&category)).unwrap(); @@ -147,6 +198,10 @@ mod tests { assert_eq!(report_event_value["kind"], 1984); assert_eq!(report_event_value["content"], "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment."); + let ReportTarget::Event(reported_event) = reported_target else { + panic!("Expected ReportedTarget::Event, got {:?}", reported_target); + }; + let expected_tags = vec![ json!(["p", reported_event.pubkey, "other"]), json!(["e", reported_event.id, "other"]), @@ -158,4 +213,38 @@ mod tests { assert_eq!(&report_event_value["tags"][i], expected_tag); } } + + #[test] + fn test_report_pubkey() { + let (report_request, reported_target, _reporter_pubkey, _reporter_text) = + setup_test_environment(false); + + let category = ModerationCategory::from_str("hate").unwrap(); + let maybe_report_event = report_request.report(Some(&category)).unwrap(); + let report_event = maybe_report_event.unwrap().event(); + let report_event_value = serde_json::to_value(report_event).unwrap(); + + assert_eq!( + report_event_value["pubkey"], + "2ddc92121b9e67172cc0d40b959c416173a3533636144ebc002b7719d8d1c4e3".to_string() + ); + assert_eq!(report_event_value["kind"], 1984); + assert_eq!(report_event_value["content"], "Content that expresses, incites, or promotes hate based on race, gender, ethnicity, religion, nationality, sexual orientation, disability status, or caste. Hateful content aimed at non-protected groups (e.g., chess players) is harassment."); + + let ReportTarget::Pubkey(reported_pubkey) = reported_target else { + panic!("Expected ReportedTarget::Pubkey, got {:?}", reported_target); + }; + + assert!(matches!(reported_target, ReportTarget::Pubkey { .. })); + + let expected_tags = vec![ + json!(["p", reported_pubkey, "other"]), + json!(["L", "MOD"]), + json!(["l", "MOD>IH", "MOD"]), + ]; + + for (i, expected_tag) in expected_tags.iter().enumerate() { + assert_eq!(&report_event_value["tags"][i], expected_tag); + } + } } diff --git a/src/lib.rs b/src/lib.rs index a278c2b..f43fd6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ mod domain_objects; pub use crate::domain_objects::as_gift_wrap::AsGiftWrap; -pub use crate::domain_objects::report_request::ReportRequest; +pub use crate::domain_objects::report_request::{ReportRequest, ReportTarget}; diff --git a/src/main.rs b/src/main.rs index 3ab75fb..d60948a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,9 @@ mod domain_objects; mod service_manager; use crate::actors::Supervisor; -use crate::adapters::{GooglePublisher, HttpServer, NostrService}; +use crate::adapters::{GooglePublisher, HttpServer, NostrService, SlackClientAdapterBuilder}; use crate::service_manager::ServiceManager; -use actors::{NostrPort, PubsubPort}; +use actors::{NostrPort, PubsubPort, SlackClientPortBuilder}; use anyhow::{Context, Result}; use nostr_sdk::prelude::*; use std::env; @@ -42,8 +42,15 @@ async fn main() -> Result<()> { let nostr_subscriber = NostrService::create(relays, gift_wrap_filter).await?; let google_publisher = GooglePublisher::create().await?; - - start_server(nostr_subscriber, google_publisher, reportinator_keys).await + let slack_writer_builder = SlackClientAdapterBuilder::default(); + + start_server( + nostr_subscriber, + google_publisher, + slack_writer_builder, + reportinator_keys, + ) + .await } /// Starts the server by spawning actors and wiring them together @@ -86,6 +93,7 @@ async fn main() -> Result<()> { async fn start_server( nostr_subscriber: impl NostrPort, google_publisher: impl PubsubPort, + slack_writer_builder: impl SlackClientPortBuilder, reportinator_keys: Keys, ) -> Result<()> { let mut manager = ServiceManager::new(); @@ -94,7 +102,12 @@ async fn start_server( let supervisor = manager .spawn_actor( Supervisor::default(), - (nostr_subscriber, google_publisher, reportinator_keys), + ( + nostr_subscriber, + google_publisher, + slack_writer_builder, + reportinator_keys, + ), ) .await?;