diff --git a/.vscode/settings.json b/.vscode/settings.json index 94d7924a..6281c6ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { - "editor.formatOnSave": true, - "rust-analyzer.checkOnSave.command": "clippy" -} \ No newline at end of file + "editor.formatOnSave": true, + "rust-analyzer.checkOnSave.command": "clippy", + "nixEnvSelector.suggestion": false +} diff --git a/Cargo.lock b/Cargo.lock index 29810124..c115abf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,6 +63,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -72,6 +83,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "ansi_term" version = "0.12.1" @@ -172,12 +189,11 @@ dependencies = [ [[package]] name = "async-lock" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", - "futures-lite", ] [[package]] @@ -399,6 +415,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base32" version = "0.4.0" @@ -423,9 +445,15 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "binascii" @@ -442,6 +470,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e0d60973d9320722cb1206f412740e162a33b8547ea8d6be75d7cff237c7a85" + [[package]] name = "bitfield" version = "0.13.2" @@ -515,7 +549,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60a2c7c80a7850b56df4b8e98e8e4932c34877b8add4f13e8350499cc1e4572" dependencies = [ - "ahash", + "ahash 0.7.6", "base64 0.13.0", "chrono", "hex", @@ -564,6 +598,48 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" +[[package]] +name = "cached" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b195e4fbc4b6862bbd065b991a34750399c119797efff72492f28a5864de8700" +dependencies = [ + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.13.2", + "instant", + "once_cell", + "thiserror", + "tokio 1.18.2", +] + +[[package]] +name = "cached_proc_macro" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082" +dependencies = [ + "cached_proc_macro_types", + "darling 0.14.4", + "proc-macro2", + "quote 1.0.26", + "syn 1.0.107", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + [[package]] name = "cc" version = "1.0.73" @@ -618,6 +694,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "coarsetime" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90d114103adbc625300f346d4d09dfb4ab1c4a8df6868435dd903392ecf4354" +dependencies = [ + "libc", + "once_cell", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + [[package]] name = "combine" version = "4.6.4" @@ -641,6 +729,37 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "const-oid" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -710,6 +829,18 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array 0.14.5", + "rand_core 0.6.3", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -720,6 +851,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df" + [[package]] name = "ctor" version = "0.1.22" @@ -739,14 +876,55 @@ dependencies = [ "cipher", ] +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2 0.4.4", + "winapi 0.3.9", +] + +[[package]] +name = "curl-sys" +version = "0.4.65+curl-8.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi 0.3.9", +] + [[package]] name = "darling" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] @@ -763,13 +941,38 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.26", + "strsim", + "syn 1.0.107", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote 1.0.26", + "syn 1.0.107", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", "quote 1.0.26", "syn 1.0.107", ] @@ -794,9 +997,9 @@ checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" [[package]] name = "deadqueue" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455d501abc72bae6432dbe500059887cab4700ba83303045db9ce2e824177d1c" +checksum = "16a2561fd313df162315935989dceb8c99db4ee1933358270a57a3cfb8c957f3" dependencies = [ "crossbeam-queue", "tokio 1.18.2", @@ -818,6 +1021,39 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808127a7de612079ec37bfc1abc48ed77a6015a971a8bd7d4178d79147cbc839" +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +dependencies = [ + "const-oid 0.6.2", + "der_derive", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid 0.9.5", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aed3b3c608dc56cf36c45fe979d04eda51242e6703d8d0bb03426ef7c41db6a" +dependencies = [ + "proc-macro2", + "quote 1.0.26", + "syn 1.0.107", + "synstructure", +] + [[package]] name = "derivative" version = "2.2.0" @@ -873,15 +1109,22 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.2", + "const-oid 0.9.5", "crypto-common", "subtle", ] +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "dotenv" version = "0.15.0" @@ -894,12 +1137,74 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ece" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd5463ffecc0677adcd786c4481f73b215714d4757edf2eb37a573c03d00459" +dependencies = [ + "base64 0.13.0", + "byteorder", + "hex", + "hkdf", + "lazy_static", + "once_cell", + "openssl", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "ed25519-compact" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3d382e8464107391c8706b4c14b087808ecb909f6c15c34114bc42e53a9e4c" +dependencies = [ + "ct-codecs", + "getrandom", +] + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der 0.6.1", + "digest 0.10.7", + "ff", + "generic-array 0.14.5", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.3", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email-encoding" version = "0.1.0" @@ -993,6 +1298,30 @@ dependencies = [ "instant", ] +[[package]] +name = "fcm" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8d0da8a6bd63bdec888b6d7a87c5698230005c1800823d28ddd5adb6f2550f" +dependencies = [ + "chrono", + "erased-serde", + "log", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "figment" version = "0.10.6" @@ -1135,6 +1464,17 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", + "tokio 1.18.2", +] + [[package]] name = "futures-macro" version = "0.3.21" @@ -1284,6 +1624,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "h2" version = "0.2.7" @@ -1343,7 +1694,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -1351,6 +1702,25 @@ name = "hashbrown" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] [[package]] name = "heck" @@ -1388,7 +1758,31 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9d405ec732fa3fcde87264e54a32a84956a377b3e3107de96e59b798c84a7" + +[[package]] +name = "hmac-sha256" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3688e69b38018fec1557254f64c8dc2cc8ec502890182f395dbb0aa997aa5735" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ce1f4656bae589a3fab938f9f09bf58645b7ed01a2c5f8a3c238e01a4ef78a" +dependencies = [ + "digest 0.10.7", ] [[package]] @@ -1617,6 +2011,35 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http", + "log", + "mime", + "once_cell", + "polling", + "serde", + "serde_json", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + [[package]] name = "iso8601-timestamp" version = "0.1.10" @@ -1671,6 +2094,55 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jwt-simple" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529a00f2d42d7dc349c994e65917c81bf53225831a65361f6c0454124c550f63" +dependencies = [ + "anyhow", + "binstring", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "spki", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1695,6 +2167,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "lettre" @@ -1735,6 +2210,22 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.8+1.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fae956c192dadcdb5dace96db71fa0b827333cce7c7b38dc71446f024d8a340" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libz-sys" version = "1.1.8" @@ -1815,6 +2306,15 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "lru" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eedb2bdbad7e0634f83989bf596f497b070130daaa398ab22d84c39e266deec5" +dependencies = [ + "hashbrown 0.14.0", +] + [[package]] name = "lru-cache" version = "0.1.2" @@ -1857,7 +2357,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658646b21e0b72f7866c7038ab086d3d5e1cd6271f060fd37defb241949d0582" dependencies = [ - "digest 0.10.3", + "digest 0.10.7", ] [[package]] @@ -1872,7 +2372,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e52eb6380b6d2a10eb3434aec0885374490f5b82c8aaf5cd487a183c98be834" dependencies = [ - "ahash", + "ahash 0.7.6", "metrics-macros", ] @@ -2105,6 +2605,23 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2115,6 +2632,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -2122,6 +2650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg 1.1.0", + "libm", ] [[package]] @@ -2187,9 +2716,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -2248,6 +2777,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.1", +] + [[package]] name = "os_info" version = "3.7.0" @@ -2259,6 +2798,28 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "parking" version = "2.0.0" @@ -2294,13 +2855,19 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271779f35b581956db91a3e55737327a03aa051e90b1c47aeb189508533adfd7" dependencies = [ - "digest 0.10.3", + "digest 0.10.7", ] [[package]] @@ -2326,6 +2893,35 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -2413,6 +3009,28 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.25" @@ -2892,36 +3510,61 @@ dependencies = [ "serde_json", ] +[[package]] +name = "revolt-config" +version = "0.6.7" +dependencies = [ + "async-std", + "cached", + "config", + "futures-locks", + "once_cell", + "serde", +] + [[package]] name = "revolt-database" version = "0.6.7" dependencies = [ + "async-lock", "async-recursion", "async-std", "async-trait", "authifier", + "base64 0.21.3", "bson", + "deadqueue", + "decancer", + "fcm", "futures", "indexmap", + "isahc", "iso8601-timestamp 0.2.11", + "linkify 0.8.1", "log", + "lru 0.11.0", "mongodb", "nanoid", "once_cell", "rand 0.8.5", "redis-kiss", "regex", + "revolt-config", "revolt-models", "revolt-permissions", "revolt-presence", "revolt-result", + "revolt_okapi", "revolt_optional_struct", + "revolt_rocket_okapi", "rocket", "schemars", "serde", "serde_json", "ulid 1.0.0", "unicode-segmentation", + "url-escape", + "web-push 0.10.0", ] [[package]] @@ -2940,10 +3583,12 @@ dependencies = [ "lettre", "linkify 0.6.0", "log", - "lru", + "lru 0.7.6", "nanoid", "num_enum 0.5.7", "once_cell", + "rand 0.8.5", + "redis-kiss", "regex", "reqwest", "revolt-database", @@ -2954,6 +3599,7 @@ dependencies = [ "revolt_rocket_okapi", "rocket", "rocket_authifier", + "rocket_cors", "rocket_empty", "rocket_prometheus", "schemars", @@ -2971,6 +3617,9 @@ version = "0.6.7" dependencies = [ "indexmap", "iso8601-timestamp 0.2.11", + "once_cell", + "regex", + "revolt-config", "revolt-permissions", "revolt_optional_struct", "schemars", @@ -2988,6 +3637,7 @@ dependencies = [ "bson", "num_enum 0.6.1", "once_cell", + "revolt-result", "schemars", "serde", ] @@ -3026,7 +3676,7 @@ dependencies = [ "iso8601-timestamp 0.1.10", "linkify 0.8.1", "log", - "lru", + "lru 0.7.6", "mongodb", "nanoid", "num_enum 0.5.7", @@ -3053,7 +3703,7 @@ dependencies = [ "serde_json", "ulid 0.5.0", "validator 0.16.0", - "web-push", + "web-push 0.7.2", ] [[package]] @@ -3112,13 +3762,24 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6620569d8ac8f0a1690fcca13f488503807a60e96ebf729749b59aca1dbef9" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote 1.0.26", "rocket_http", "syn 1.0.107", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + [[package]] name = "ring" version = "0.16.20" @@ -3291,6 +3952,38 @@ dependencies = [ "rocket", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.0", + "bitflags", + "serde", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.3", + "signature", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rust-argon2" version = "1.0.0" @@ -3303,6 +3996,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -3433,6 +4136,31 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der 0.6.1", + "generic-array 0.14.5", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1_decode" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6326ddc956378a0739200b2c30892dccaf198992dfd7323274690b9e188af23" +dependencies = [ + "der 0.4.5", + "pem 0.8.3", + "thiserror", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -3665,7 +4393,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote 1.0.26", "syn 1.0.107", @@ -3691,7 +4419,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.3", + "digest 0.10.7", ] [[package]] @@ -3708,7 +4436,7 @@ checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.3", + "digest 0.10.7", ] [[package]] @@ -3739,6 +4467,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.3", +] + [[package]] name = "slab" version = "0.4.6" @@ -3746,10 +4484,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] -name = "smallvec" -version = "1.8.0" +name = "sluice" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -3784,6 +4533,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "stable-pattern" version = "0.1.0" @@ -3866,6 +4625,18 @@ dependencies = [ "unicode-xid 0.0.4", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote 1.0.26", + "syn 1.0.107", + "unicode-xid 0.2.3", +] + [[package]] name = "sysinfo" version = "0.27.6" @@ -4020,6 +4791,7 @@ dependencies = [ "mio 0.8.3", "num_cpus", "once_cell", + "parking_lot", "pin-project-lite 0.2.9", "signal-hook-registry", "socket2 0.4.4", @@ -4138,7 +4910,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc496875d9c8fe9a0ce19e3ee8e8808c60376831a439543f0aac71c9dd129fa" dependencies = [ - "digest 0.10.3", + "digest 0.10.7", "hmac", "sha-1 0.10.0", "sha2", @@ -4460,7 +5232,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "log", "native-tls", "once_cell", @@ -4480,6 +5252,15 @@ dependencies = [ "serde", ] +[[package]] +name = "url-escape" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218" +dependencies = [ + "percent-encoding", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -4728,6 +5509,28 @@ dependencies = [ "untrusted 0.6.2", ] +[[package]] +name = "web-push" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdf799b4be6f04bfee94807548718de568834c9ea020e73b59af3dac66d72c" +dependencies = [ + "async-trait", + "base64 0.13.0", + "chrono", + "ece", + "futures-lite", + "http", + "isahc", + "jwt-simple", + "log", + "pem 1.1.1", + "sec1_decode", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "web-sys" version = "0.3.57" @@ -4886,8 +5689,23 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/clippy.toml b/clippy.toml index a1c69535..d4739dfa 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,13 +3,23 @@ disallowed-methods = [ "revolt_database::models::bots::model::Bot::remove_field", # Prefer to use Object::create() - "revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike", + "revolt_database::models::bots::ops::AbstractBots::insert_bot", + "revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite", + "revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message", + "revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook", + "revolt_database::models::channels::ops::AbstractChannels::insert_channel", + "revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji", + "revolt_database::models::files::ops::AbstractAttachments::insert_attachment", + "revolt_database::models::messages::ops::AbstractMessages::insert_message", + "revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event", + "revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban", + "revolt_database::models::server_members::ops::AbstractServerMembers::insert_member", + "revolt_database::models::servers::ops::AbstractServers::insert_server", + "revolt_database::models::users::ops::AbstractUsers::insert_user", # Prefer to use Object::update(&self) "revolt_database::models::bots::ops::AbstractBots::update_bot", - "revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike", # Prefer to use Object::delete(&self) "revolt_database::models::bots::ops::AbstractBots::delete_bot", - "revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike", ] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml new file mode 100644 index 00000000..8ffe90d7 --- /dev/null +++ b/crates/core/config/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "revolt-config" +version = "0.6.7" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = ["Paul Makles "] +description = "Revolt Backend: Configuration" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +test = ["async-std"] +default = ["test"] + +[dependencies] +# Utility +config = "0.13.3" +cached = "0.44.0" +once_cell = "1.18.0" + +# Serde +serde = { version = "1", features = ["derive"] } + +# Async +futures-locks = "0.7.1" +async-std = { version = "1.8.0", features = ["attributes"], optional = true } diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml new file mode 100644 index 00000000..213f1fd0 --- /dev/null +++ b/crates/core/config/Revolt.toml @@ -0,0 +1,64 @@ +[database] +mongodb = "mongodb://database" +redis = "redis://redis/" + +[hosts] +app = "http://local.revolt.chat" +api = "http://local.revolt.chat/api" +events = "ws://local.revolt.chat/ws" +autumn = "http://local.revolt.chat/autumn" +january = "http://local.revolt.chat/january" +voso_legacy = "" +voso_legacy_ws = "" + +[api] +staging = false + +[api.registration] +invite_only = false + +[api.smtp] +host = "" +username = "" +password = "" +from_address = "" + +[api.vapid] +private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo" +public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=" + +[api.fcm] +api_key = "" + +[api.security] +authifier_shield_key = "" +voso_legacy_token = "" + +[api.security.captcha] +hcaptcha_key = "" +hcaptcha_sitekey = "" + +[api.workers] +max_concurrent_connections = 50 + +[features] + +[features.limits] + +[features.limits.default] +group_size = 100 +bots = 5 +message_replies = 5 +message_attachments = 5 +message_embeds = 5 +servers = 100 +server_emoji = 100 +server_roles = 200 +server_channels = 200 + +attachment_size = 20000000 +avatar_size = 4000000 +background_size = 6000000 +icon_size = 2500000 +banner_size = 6000000 +emoji_size = 500000 diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs new file mode 100644 index 00000000..3bd5c019 --- /dev/null +++ b/crates/core/config/src/lib.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; + +use cached::proc_macro::cached; +use config::{Config, File, FileFormat}; +use futures_locks::RwLock; +use once_cell::sync::Lazy; +use serde::Deserialize; + +static CONFIG_BUILDER: Lazy> = Lazy::new(|| { + RwLock::new({ + let mut builder = Config::builder().add_source(File::from_str( + include_str!("../Revolt.toml"), + FileFormat::Toml, + )); + + if std::path::Path::new("revolt.toml").exists() { + builder = builder.add_source(File::new("revolt.toml", FileFormat::Toml)); + } + + builder.build().unwrap() + }) +}); + +// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow + +#[derive(Deserialize, Debug, Clone)] +pub struct Database { + pub mongodb: String, + pub redis: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Hosts { + pub app: String, + pub api: String, + pub events: String, + pub autumn: String, + pub january: String, + pub voso_legacy: String, + pub voso_legacy_ws: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiRegistration { + pub invite_only: bool, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiSmtp { + pub host: String, + pub username: String, + pub password: String, + pub from_address: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiVapid { + pub private_key: String, + pub public_key: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiFcm { + pub api_key: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiSecurityCaptcha { + pub hcaptcha_key: String, + pub hcaptcha_sitekey: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiSecurity { + pub authifier_shield_key: String, + pub voso_legacy_token: String, + pub captcha: ApiSecurityCaptcha, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct ApiWorkers { + pub max_concurrent_connections: usize, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Api { + pub staging: bool, + pub registration: ApiRegistration, + pub smtp: ApiSmtp, + pub vapid: ApiVapid, + pub fcm: ApiFcm, + pub security: ApiSecurity, + pub workers: ApiWorkers, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct FeaturesLimits { + pub group_size: usize, + pub bots: usize, + pub message_replies: usize, + pub message_attachments: usize, + pub message_embeds: usize, + pub servers: usize, + pub server_emoji: usize, + pub server_roles: usize, + pub server_channels: usize, + + pub attachment_size: usize, + pub avatar_size: usize, + pub background_size: usize, + pub icon_size: usize, + pub banner_size: usize, + pub emoji_size: usize, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct FeaturesLimitsCollection { + pub default: FeaturesLimits, + + #[serde(flatten)] + pub roles: HashMap, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Features { + pub limits: FeaturesLimitsCollection, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Settings { + pub database: Database, + pub hosts: Hosts, + pub api: Api, + pub features: Features, +} + +pub async fn init() { + println!( + ":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m", + config().await + ); +} + +pub async fn read() -> Config { + CONFIG_BUILDER.read().await.clone() +} + +#[cached(time = 30)] +pub async fn config() -> Settings { + read().await.try_deserialize::().unwrap() +} + +#[cfg(feature = "test")] +#[cfg(test)] +mod tests { + use crate::init; + + #[async_std::test] + async fn it_works() { + init().await; + } +} diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index ea27b17e..5a1043c6 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -13,15 +13,17 @@ description = "Revolt Backend: Database Implementation" mongodb = ["dep:mongodb", "bson"] # ... Other +tasks = ["isahc", "linkify", "url-escape"] async-std-runtime = ["async-std"] -rocket-impl = ["rocket", "schemars"] +rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"] redis-is-patched = ["revolt-presence/redis-is-patched"] # Default Features -default = ["mongodb", "async-std-runtime"] +default = ["mongodb", "async-std-runtime", "tasks"] [dependencies] # Core +revolt-config = { version = "0.6.7", path = "../config" } revolt-result = { version = "0.6.7", path = "../result" } revolt-models = { version = "0.6.7", path = "../models" } revolt-presence = { version = "0.6.7", path = "../presence" } @@ -32,11 +34,18 @@ revolt-permissions = { version = "0.6.7", path = "../permissions", features = [ # Utility log = "0.4" +lru = "0.11.0" rand = "0.8.5" ulid = "1.0.0" nanoid = "0.4.0" +base64 = "0.21.3" once_cell = "1.17" indexmap = "1.9.1" +decancer = "1.6.2" +deadqueue = "0.2.4" +linkify = { optional = true, version = "0.8.1" } +url-escape = { optional = true, version = "0.1.1" } +isahc = { optional = true, version = "1.7", features = ["json"] } # Serialisation serde_json = "1" @@ -57,6 +66,7 @@ regex = "1" # Async Language Features futures = "0.3.19" +async-lock = "2.8.0" async-trait = "0.1.51" async-recursion = "1.0.4" @@ -68,6 +78,12 @@ schemars = { version = "0.8.8", optional = true } rocket = { version = "0.5.0-rc.2", default-features = false, features = [ "json", ], optional = true } +revolt_okapi = { version = "0.9.1", optional = true } +revolt_rocket_okapi = { version = "0.9.1", optional = true } + +# Notifications +fcm = "0.9.2" +web-push = "0.10.0" # Authifier authifier = { version = "1.0" } diff --git a/crates/core/database/src/drivers/mod.rs b/crates/core/database/src/drivers/mod.rs index 79cc03f1..01dc4455 100644 --- a/crates/core/database/src/drivers/mod.rs +++ b/crates/core/database/src/drivers/mod.rs @@ -65,3 +65,14 @@ impl DatabaseInfo { }) } } + +impl From for authifier::Database { + fn from(value: Database) -> Self { + match value { + Database::Reference(_) => Default::default(), + Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb( + authifier::database::MongoDb(client.database("revolt")), + ), + } + } +} diff --git a/crates/core/database/src/drivers/mongodb.rs b/crates/core/database/src/drivers/mongodb.rs index 74e5c6ef..a4bbe56b 100644 --- a/crates/core/database/src/drivers/mongodb.rs +++ b/crates/core/database/src/drivers/mongodb.rs @@ -238,6 +238,6 @@ pub fn prefix_keys(t: &T, prefix: &str) -> HashMap = serde_json::from_str(&v).unwrap(); v.into_iter() .filter(|(_k, v)| !v.is_null()) - .map(|(k, v)| (prefix.to_owned() + &k, v)) + .map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v)) .collect() } diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs index 0f3a8664..6f6dd9c2 100644 --- a/crates/core/database/src/drivers/reference.rs +++ b/crates/core/database/src/drivers/reference.rs @@ -4,7 +4,7 @@ use futures::lock::Mutex; use crate::{ Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member, - MemberCompositeKey, Server, ServerBan, User, UserSettings, Webhook, + MemberCompositeKey, Message, Server, ServerBan, User, UserSettings, Webhook, }; database_derived!( @@ -17,14 +17,14 @@ database_derived!( pub channel_unreads: Arc>>, pub channel_webhooks: Arc>>, pub emojis: Arc>>, + pub files: Arc>>, + pub messages: Arc>>, pub user_settings: Arc>>, pub users: Arc>>, pub server_bans: Arc>>, pub server_members: Arc>>, pub servers: Arc>>, - pub files: Arc>>, pub safety_reports: Arc>>, pub safety_snapshots: Arc>>, - pub messages: Arc>>, } ); diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index 376b9fe3..3f2e812c 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -2,9 +2,10 @@ use authifier::AuthifierEvent; use serde::{Deserialize, Serialize}; use revolt_models::v0::{ - Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, FieldsWebhook, - MemberCompositeKey, PartialChannel, PartialMember, PartialRole, PartialServer, PartialWebhook, - Server, UserSettings, Webhook, + AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, + FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember, + PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, UserSettings, + Webhook, }; use revolt_result::Error; @@ -57,8 +58,7 @@ pub enum EventV1 { }, /// Ping response - Pong { data: Ping }, - + Pong { data: Ping }, */ /// New message Message(Message), @@ -103,7 +103,8 @@ pub enum EventV1 { }, /// Bulk delete messages - BulkMessageDelete { channel: String, ids: Vec },*/ + BulkMessageDelete { channel: String, ids: Vec }, + /// New server ServerCreate { id: String, @@ -145,7 +146,7 @@ pub enum EventV1 { /// Server role deleted ServerRoleDelete { id: String, role_id: String }, - /*/// Update existing user + /// Update existing user UserUpdate { id: String, data: PartialUser, @@ -153,7 +154,7 @@ pub enum EventV1 { event_id: Option, }, - /// Relationship with another user changed + /*/// Relationship with another user changed UserRelationship { id: String, user: User, diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs index 03f0df41..90365598 100644 --- a/crates/core/database/src/lib.rs +++ b/crates/core/database/src/lib.rs @@ -43,7 +43,7 @@ macro_rules! auto_derived { macro_rules! auto_derived_partial { ( $item:item, $name:expr ) => { - #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] + #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_name = $name] #[opt_skip_serializing_none] @@ -81,6 +81,7 @@ pub mod util; pub use models::*; pub mod events; +pub mod tasks; /// Utility function to check if a boolean value is false pub fn if_false(t: &bool) -> bool { diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs index 23cd9861..debfdfe8 100644 --- a/crates/core/database/src/models/bots/model.rs +++ b/crates/core/database/src/models/bots/model.rs @@ -1,6 +1,7 @@ use revolt_result::Result; +use ulid::Ulid; -use crate::Database; +use crate::{BotInformation, Database, PartialUser, User}; auto_derived_partial!( /// Bot @@ -49,8 +50,71 @@ auto_derived!( } ); +#[allow(clippy::derivable_impls)] +impl Default for Bot { + fn default() -> Self { + Self { + id: Default::default(), + owner: Default::default(), + token: Default::default(), + public: Default::default(), + analytics: Default::default(), + discoverable: Default::default(), + interactions_url: Default::default(), + terms_of_service_url: Default::default(), + privacy_policy_url: Default::default(), + flags: Default::default(), + } + } +} + #[allow(clippy::disallowed_methods)] impl Bot { + /// Create a new bot + pub async fn create(db: &Database, username: String, owner: &User, data: D) -> Result + where + D: Into>, + { + if owner.bot.is_some() { + return Err(create_error!(IsBot)); + } + + // TODO: config + let max_bot_count = 5; + if db.get_number_of_bots_by_user(&owner.id).await? >= max_bot_count { + return Err(create_error!(ReachedMaximumBots)); + } + + let id = Ulid::new().to_string(); + + User::create( + db, + username, + Some(id.to_string()), + Some(PartialUser { + bot: Some(BotInformation { + owner: id.to_string(), + }), + ..Default::default() + }), + ) + .await?; + + let mut bot = Bot { + id, + owner: owner.id.to_string(), + token: nanoid::nanoid!(64), + ..Default::default() + }; + + if let Some(data) = data.into() { + bot.apply_options(data); + } + + db.insert_bot(&bot).await?; + Ok(bot) + } + /// Remove a field from this object pub fn remove_field(&mut self, field: &FieldsBot) { match field { @@ -96,27 +160,24 @@ mod tests { #[async_std::test] async fn crud() { database_test!(|db| async move { - let bot_id = "bot"; - let user_id = "user"; - let token = "my_token"; + let owner = User::create(&db, "Owner".to_string(), None, None) + .await + .unwrap(); - let user = User { - id: bot_id.to_string(), - username: "Bot Name".to_string(), - ..Default::default() - }; + let bot = Bot::create( + &db, + "Bot Name".to_string(), + &owner, + PartialBot { + token: Some("my token".to_string()), + interactions_url: Some("some url".to_string()), + ..Default::default() + }, + ) + .await + .unwrap(); - db.insert_user(&user).await.unwrap(); - - let bot = Bot { - id: bot_id.to_string(), - owner: user_id.to_string(), - token: token.to_string(), - interactions_url: "some url".to_string(), - ..Default::default() - }; - - db.insert_bot(&bot).await.unwrap(); + assert!(!bot.interactions_url.is_empty()); let mut updated_bot = bot.clone(); updated_bot @@ -131,9 +192,9 @@ mod tests { .await .unwrap(); - let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap(); + let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap(); let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap(); - let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap(); + let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap(); assert!(!bot.public); assert!(fetched_bot1.public); @@ -143,12 +204,12 @@ mod tests { assert_eq!(updated_bot, fetched_bot1); assert_eq!(fetched_bot1, fetched_bot2); assert_eq!(fetched_bot1, fetched_bots[0]); - assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap()); + assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap()); bot.delete(&db).await.unwrap(); - assert!(db.fetch_bot(bot_id).await.is_err()); - assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap()); - assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2)) + assert!(db.fetch_bot(&bot.id).await.is_err()); + assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap()); + assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2)) }); } } diff --git a/crates/core/database/src/models/channel_unreads/ops/reference.rs b/crates/core/database/src/models/channel_unreads/ops/reference.rs index 4fb216ae..b7ba5b95 100644 --- a/crates/core/database/src/models/channel_unreads/ops/reference.rs +++ b/crates/core/database/src/models/channel_unreads/ops/reference.rs @@ -20,7 +20,7 @@ impl AbstractChannelUnreads for ReferenceDb { user: user_id.to_string(), }; - if let Some(mut unread) = unreads.get_mut(&key) { + if let Some(unread) = unreads.get_mut(&key) { unread.mentions = None; unread.last_id.replace(message_id.to_string()); } else { @@ -41,6 +41,7 @@ impl AbstractChannelUnreads for ReferenceDb { async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> { let current_time = Ulid::new().to_string(); for channel_id in channel_ids { + #[allow(clippy::disallowed_methods)] self.acknowledge_message(channel_id, user_id, ¤t_time) .await?; } diff --git a/crates/core/database/src/models/channel_webhooks/model.rs b/crates/core/database/src/models/channel_webhooks/model.rs index 87ea23d0..568326d6 100644 --- a/crates/core/database/src/models/channel_webhooks/model.rs +++ b/crates/core/database/src/models/channel_webhooks/model.rs @@ -36,6 +36,20 @@ auto_derived!( } ); +#[allow(clippy::derivable_impls)] +impl Default for Webhook { + fn default() -> Self { + Self { + id: Default::default(), + name: Default::default(), + avatar: None, + channel_id: Default::default(), + permissions: Default::default(), + token: Default::default(), + } + } +} + #[allow(clippy::disallowed_methods)] impl Webhook { pub async fn create(&self, db: &Database) -> Result<()> { @@ -120,7 +134,7 @@ mod tests { id: webhook_id.to_string(), name: "Webhook Name".to_string(), channel_id: channel_id.to_string(), - avatar: Some(Default::default()), + avatar: None, ..Default::default() }; diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 2323020f..ac07e16d 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -1,12 +1,14 @@ use std::collections::HashMap; +use revolt_models::v0::MessageAuthor; use revolt_permissions::OverrideField; use revolt_result::Result; use serde::{Deserialize, Serialize}; -use crate::{events::client::EventV1, Database, File, IntoDocumentPath}; +use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User}; auto_derived!( + #[serde(tag = "channel_type")] pub enum Channel { /// Personal "Saved Notes" channel which allows users to save messages SavedMessages { @@ -164,6 +166,7 @@ auto_derived!( } ); +#[allow(clippy::disallowed_methods)] impl Channel { /// Create a channel pub async fn create(&self, db: &Database) -> Result<()> { @@ -189,40 +192,48 @@ impl Channel { pub async fn add_user_to_group( &mut self, db: &Database, - user_id: &str, - _by_id: &str, + user: &User, + by_id: &str, ) -> Result<()> { if let Channel::Group { recipients, .. } = self { - if recipients.contains(&String::from(user_id)) { + if recipients.contains(&String::from(&user.id)) { return Err(create_error!(AlreadyInGroup)); } - recipients.push(String::from(user_id)); + recipients.push(String::from(&user.id)); } match &self { Channel::Group { id, .. } => { - db.add_user_to_group(id, user_id).await?; + db.add_user_to_group(id, &user.id).await?; EventV1::ChannelGroupJoin { id: id.to_string(), - user: user_id.to_string(), + user: user.id.to_string(), } .p(id.to_string()) .await; EventV1::ChannelCreate(self.clone().into()) - .private(user_id.to_string()) + .private(user.id.to_string()) .await; - /* TODO: SystemMessage::UserAdded { - id: user.to_string(), - by: by.to_string(), + SystemMessage::UserAdded { + id: user.id.to_string(), + by: by_id.to_string(), } .into_message(id.to_string()) - .create(db, self, None) + .send( + db, + MessageAuthor::System { + username: &user.username, + avatar: user.avatar.as_ref().map(|file| file.id.as_ref()), + }, + self, + false, + ) .await - .ok(); */ + .ok(); Ok(()) } @@ -468,19 +479,20 @@ impl Channel { pub async fn remove_user_from_group( &self, db: &Database, - user_id: &str, - _by_id: Option<&str>, + user: &User, + by_id: Option<&str>, silent: bool, ) -> Result<()> { match &self { Channel::Group { id, + name, owner, recipients, .. } => { - if user_id == owner { - if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) { + if &user.id == owner { + if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) { db.update_channel( id, &PartialChannel { @@ -491,14 +503,22 @@ impl Channel { ) .await?; - /* TODO: SystemMessage::ChannelOwnershipChanged { + SystemMessage::ChannelOwnershipChanged { from: owner.to_string(), - to: new_owner.into(), + to: new_owner.to_string(), } .into_message(id.to_string()) - .create(db, self, None) + .send( + db, + MessageAuthor::System { + username: name, + avatar: None, + }, + self, + false, + ) .await - .ok(); */ + .ok(); } else { db.delete_channel(self).await?; return Ok(()); @@ -507,26 +527,34 @@ impl Channel { EventV1::ChannelGroupLeave { id: id.to_string(), - user: user_id.to_string(), + user: user.id.to_string(), } .p(id.to_string()) .await; if !silent { - /* TODO: if let Some(_by) = by_id { + if let Some(by) = by_id { SystemMessage::UserRemove { - id: user_id.to_string(), + id: user.id.to_string(), by: by.to_string(), } } else { SystemMessage::UserLeft { - id: user_id.to_string(), + id: user.id.to_string(), } } .into_message(id.to_string()) - .create(db, self, None) + .send( + db, + MessageAuthor::System { + username: &user.username, + avatar: user.avatar.as_ref().map(|file| file.id.as_ref()), + }, + self, + false, + ) .await - .ok(); */ + .ok(); } Ok(()) diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs index 50e43fc3..a2d3aefd 100644 --- a/crates/core/database/src/models/messages/model.rs +++ b/crates/core/database/src/models/messages/model.rs @@ -1,8 +1,14 @@ use indexmap::{IndexMap, IndexSet}; use iso8601_timestamp::Timestamp; -use revolt_models::v0::{Embed, MessageSort, MessageWebhook}; +use revolt_models::v0::{Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification}; +use revolt_result::Result; +use ulid::Ulid; -use crate::File; +use crate::{ + events::client::EventV1, + tasks::{self, ack::AckEvent}, + Channel, Database, File, +}; auto_derived_partial!( /// Message @@ -165,33 +171,144 @@ auto_derived!( } ); +#[allow(clippy::derivable_impls)] +impl Default for Message { + fn default() -> Self { + Self { + id: Default::default(), + nonce: None, + channel: Default::default(), + author: Default::default(), + webhook: None, + content: None, + system: None, + attachments: None, + edited: None, + embeds: None, + mentions: None, + replies: None, + reactions: Default::default(), + interactions: Default::default(), + masquerade: None, + } + } +} + #[allow(clippy::disallowed_methods)] -impl Message {} - -impl Interactions { - /// Validate interactions info is correct - /* pub async fn validate( - &self, +impl Message { + /// Send a message without any notifications + pub async fn send_without_notifications( + &mut self, db: &Database, - permissions: &mut PermissionCalculator<'_>, + is_dm: bool, + generate_embeds: bool, ) -> Result<()> { - if let Some(reactions) = &self.reactions { - permissions.throw_permission(db, Permission::React).await?; + db.insert_message(self).await?; - if reactions.len() > 20 { - return Err(Error::InvalidOperation); + // Fan out events + EventV1::Message(self.clone().into()) + .p(self.channel.to_string()) + .await; + + // Update last_message_id + tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await; + + // Add mentions for affected users + if let Some(mentions) = &self.mentions { + for user in mentions { + tasks::ack::queue( + self.channel.to_string(), + user.to_string(), + AckEvent::AddMention { + ids: vec![self.id.to_string()], + }, + ) + .await; } + } - for reaction in reactions { - if !Emoji::can_use(db, reaction).await? { - return Err(Error::InvalidOperation); - } + // Generate embeds + if generate_embeds { + if let Some(content) = &self.content { + tasks::process_embeds::queue( + self.channel.to_string(), + self.id.to_string(), + content.clone(), + ) + .await; } } Ok(()) - }*/ + } + /// Send a message + pub async fn send( + &mut self, + db: &Database, + author: MessageAuthor<'_>, + channel: &Channel, + generate_embeds: bool, + ) -> Result<()> { + self.send_without_notifications( + db, + matches!(channel, Channel::DirectMessage { .. }), + generate_embeds, + ) + .await?; + + // Push out Web Push notifications + crate::tasks::web_push::queue( + { + match channel { + Channel::DirectMessage { recipients, .. } + | Channel::Group { recipients, .. } => recipients.clone(), + Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(), + _ => vec![], + } + }, + PushNotification::from(self.clone().into(), Some(author), &channel.id()).await, + ) + .await; + + Ok(()) + } + + /// Append content to message + pub async fn append( + db: &Database, + id: String, + channel: String, + append: AppendMessage, + ) -> Result<()> { + db.append_message(&id, &append).await?; + + EventV1::MessageAppend { + id, + channel: channel.to_string(), + append: append.into(), + } + .p(channel) + .await; + + Ok(()) + } +} + +impl SystemMessage { + pub fn into_message(self, channel: String) -> Message { + Message { + id: Ulid::new().to_string(), + channel, + author: "00000000000000000000000000".to_string(), + system: Some(self), + + ..Default::default() + } + } +} + +impl Interactions { /// Check if we can use a given emoji to react pub fn can_use(&self, emoji: &str) -> bool { if self.restrict_reactions { diff --git a/crates/core/database/src/models/messages/ops.rs b/crates/core/database/src/models/messages/ops.rs index 875b3454..7d5ec2ef 100644 --- a/crates/core/database/src/models/messages/ops.rs +++ b/crates/core/database/src/models/messages/ops.rs @@ -2,8 +2,8 @@ use revolt_result::Result; use crate::{AppendMessage, Message, MessageQuery, PartialMessage}; -// mod mongodb; -// mod reference; +mod mongodb; +mod reference; #[async_trait] pub trait AbstractMessages: Sync + Send { @@ -35,5 +35,5 @@ pub trait AbstractMessages: Sync + Send { async fn delete_message(&self, id: &str) -> Result<()>; /// Delete messages from a channel by their ids and corresponding channel id - async fn delete_messages(&self, channel: &str, ids: Vec) -> Result<()>; + async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>; } diff --git a/crates/core/database/src/models/messages/ops/mongodb.rs b/crates/core/database/src/models/messages/ops/mongodb.rs index 882cc8ec..42c2f270 100644 --- a/crates/core/database/src/models/messages/ops/mongodb.rs +++ b/crates/core/database/src/models/messages/ops/mongodb.rs @@ -1,64 +1,214 @@ -use bson::Document; +use bson::{to_bson, Document}; +use futures::try_join; +use mongodb::options::FindOptions; +use revolt_models::v0::MessageSort; use revolt_result::Result; -use crate::Emoji; -use crate::MongoDb; +use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage}; -use super::AbstractEmojis; +use super::AbstractMessages; -static COL: &str = "emojis"; +static COL: &str = "messages"; #[async_trait] -impl AbstractEmojis for MongoDb { - /// Insert emoji into database. - async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> { - query!(self, insert_one, COL, &emoji).map(|_| ()) +impl AbstractMessages for MongoDb { + /// Insert a new message into the database + async fn insert_message(&self, message: &Message) -> Result<()> { + query!(self, insert_one, COL, &message).map(|_| ()) } - /// Fetch an emoji by its id - async fn fetch_emoji(&self, id: &str) -> Result { + /// Fetch a message by its id + async fn fetch_message(&self, id: &str) -> Result { query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound)) } - /// Fetch emoji by their parent id - async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result> { - query!( - self, - find_one, - COL, - doc! { - "parent.id": parent_id - } - )? - .ok_or_else(|| create_error!(NotFound)) - } + /// Fetch multiple messages by given query + async fn fetch_messages(&self, query: MessageQuery) -> Result> { + let mut filter = doc! {}; - /// Fetch emoji by their parent ids - async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result> { - query!( - self, - find, - COL, - doc! { - "parent.id": { - "$in": parent_ids + // 1. Apply message filters + if let Some(channel) = query.filter.channel { + filter.insert("channel", channel); + } + + if let Some(author) = query.filter.author { + filter.insert("author", author); + } + + let is_search_query = if let Some(query) = query.filter.query { + filter.insert( + "$text", + doc! { + "$search": query + }, + ); + + true + } else { + false + }; + + // 2. Find query limit + let limit = query.limit.unwrap_or(50); + + // 3. Apply message time period + match query.time_period { + MessageTimePeriod::Relative { nearby } => { + // 3.1. Prepare filters + let mut older_message_filter = filter.clone(); + let mut newer_message_filter = filter; + + older_message_filter.insert( + "_id", + doc! { + "$lt": &nearby + }, + ); + + newer_message_filter.insert( + "_id", + doc! { + "$gte": &nearby + }, + ); + + // 3.2. Execute in both directions + let (a, b) = try_join!( + self.find_with_options::<_, Message>( + COL, + newer_message_filter, + FindOptions::builder() + .limit(limit / 2 + 1) + .sort(doc! { + "_id": 1_i32 + }) + .build(), + ), + self.find_with_options::<_, Message>( + COL, + older_message_filter, + FindOptions::builder() + .limit(limit / 2) + .sort(doc! { + "_id": -1_i32 + }) + .build(), + ) + ) + .map_err(|_| create_database_error!("find", COL))?; + + Ok([a, b].concat()) + } + MessageTimePeriod::Absolute { + before, + after, + sort, + } => { + // 3.1. Apply message ID filter + if let Some(doc) = match (before, after) { + (Some(before), Some(after)) => Some(doc! { + "$lt": before, + "$gt": after + }), + (Some(before), _) => Some(doc! { + "$lt": before + }), + (_, Some(after)) => Some(doc! { + "$gt": after + }), + _ => None, + } { + filter.insert("_id", doc); } + + // 3.2. Execute with given message sort + self.find_with_options( + COL, + filter, + FindOptions::builder() + .limit(limit) + .sort(match sort.unwrap_or(MessageSort::Latest) { + // Sort by relevance, fallback to latest + MessageSort::Relevance => { + if is_search_query { + doc! { + "score": { + "$meta": "textScore" + } + } + } else { + doc! { + "_id": -1_i32 + } + } + } + // Sort by latest first + MessageSort::Latest => doc! { + "_id": -1_i32 + }, + // Sort by oldest first + MessageSort::Oldest => doc! { + "_id": 1_i32 + }, + }) + .build(), + ) + .await + .map_err(|_| create_database_error!("find", COL)) } - ) + } } - /// Detach an emoji by its id - async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { + /// Update a given message with new information + async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> { + query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ()) + } + + /// Append information to a given message + async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> { + let mut query = doc! {}; + + if let Some(embeds) = &append.embeds { + if !embeds.is_empty() { + query.insert( + "$push", + doc! { + "embeds": { + "$each": to_bson(embeds) + .map_err(|_| create_database_error!("to_bson", "embeds"))? + } + }, + ); + } + } + + if query.is_empty() { + return Ok(()); + } + self.col::(COL) .update_one( doc! { - "_id": &emoji.id + "_id": id + }, + query, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", COL)) + } + + /// Add a new reaction to a message + async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id }, doc! { - "$set": { - "parent": { - "type": "Detached" - } + "$addToSet": { + format!("reactions.{emoji}"): user } }, None, @@ -67,4 +217,64 @@ impl AbstractEmojis for MongoDb { .map(|_| ()) .map_err(|_| create_database_error!("update_one", COL)) } + + /// Remove a reaction from a message + async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id + }, + doc! { + "$pull": { + format!("reactions.{emoji}"): user + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", COL)) + } + + /// Remove reaction from a message + async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> { + self.col::(COL) + .update_one( + doc! { + "_id": id + }, + doc! { + "$unset": { + format!("reactions.{emoji}"): 1 + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("update_one", COL)) + } + + /// Delete a message from the database by its id + async fn delete_message(&self, id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, id).map(|_| ()) + } + + /// Delete messages from a channel by their ids and corresponding channel id + async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> { + self.col::(COL) + .delete_many( + doc! { + "channel": channel, + "_id": { + "$in": ids + } + }, + None, + ) + .await + .map(|_| ()) + .map_err(|_| create_database_error!("delete_many", COL)) + } } diff --git a/crates/core/database/src/models/messages/ops/reference.rs b/crates/core/database/src/models/messages/ops/reference.rs index 2f0c2a2d..b3ddeda3 100644 --- a/crates/core/database/src/models/messages/ops/reference.rs +++ b/crates/core/database/src/models/messages/ops/reference.rs @@ -1,67 +1,272 @@ +use indexmap::IndexSet; use revolt_result::Result; -use crate::Emoji; -use crate::EmojiParent; -use crate::ReferenceDb; +use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb}; -use super::AbstractEmojis; +use super::AbstractMessages; #[async_trait] -impl AbstractEmojis for ReferenceDb { - /// Insert emoji into database. - async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> { - let mut emojis = self.emojis.lock().await; - if emojis.contains_key(&emoji.id) { - Err(create_database_error!("insert", "emoji")) +impl AbstractMessages for ReferenceDb { + /// Insert a new message into the database + async fn insert_message(&self, message: &Message) -> Result<()> { + let mut messages = self.messages.lock().await; + if messages.contains_key(&message.id) { + Err(create_database_error!("insert", "message")) } else { - emojis.insert(emoji.id.to_string(), emoji.clone()); + messages.insert(message.id.to_string(), message.clone()); Ok(()) } } - /// Fetch an emoji by its id - async fn fetch_emoji(&self, id: &str) -> Result { - let emojis = self.emojis.lock().await; - emojis + /// Fetch a message by its id + async fn fetch_message(&self, id: &str) -> Result { + let messages = self.messages.lock().await; + messages .get(id) .cloned() .ok_or_else(|| create_error!(NotFound)) } - /// Fetch emoji by their parent id - async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result> { - let emojis = self.emojis.lock().await; - Ok(emojis + /// Fetch multiple messages by given query + async fn fetch_messages(&self, query: MessageQuery) -> Result> { + let messages = self.messages.lock().await; + let matched_messages = messages .values() - .filter(|emoji| match &emoji.parent { - EmojiParent::Server { id } => id == parent_id, - _ => false, + .filter(|message| { + if let Some(channel) = &query.filter.channel { + if &message.channel != channel { + return false; + } + } + + if let Some(author) = &query.filter.author { + if &message.author != author { + return false; + } + } + + if let Some(query) = &query.filter.query { + if let Some(content) = &message.content { + if !content.to_lowercase().contains(query) { + return false; + } + } else { + return false; + } + } + + true }) .cloned() - .collect()) + .collect(); + + // TODO: sorting, etc + + Ok(matched_messages) + + /* + // 2. Find query limit + let limit = query.limit.unwrap_or(50); + + // 3. Apply message time period + match query.time_period { + MessageTimePeriod::Relative { nearby } => { + // 3.1. Prepare filters + let mut older_message_filter = filter.clone(); + let mut newer_message_filter = filter; + + older_message_filter.insert( + "_id", + doc! { + "$lt": &nearby + }, + ); + + newer_message_filter.insert( + "_id", + doc! { + "$gte": &nearby + }, + ); + + // 3.2. Execute in both directions + let (a, b) = try_join!( + self.find_with_options::<_, Message>( + COL, + newer_message_filter, + FindOptions::builder() + .limit(limit / 2 + 1) + .sort(doc! { + "_id": 1_i32 + }) + .build(), + ), + self.find_with_options::<_, Message>( + COL, + older_message_filter, + FindOptions::builder() + .limit(limit / 2) + .sort(doc! { + "_id": -1_i32 + }) + .build(), + ) + ) + .map_err(|_| create_database_error!("find", COL))?; + + Ok([a, b].concat()) + } + MessageTimePeriod::Absolute { + before, + after, + sort, + } => { + // 3.1. Apply message ID filter + if let Some(doc) = match (before, after) { + (Some(before), Some(after)) => Some(doc! { + "$lt": before, + "$gt": after + }), + (Some(before), _) => Some(doc! { + "$lt": before + }), + (_, Some(after)) => Some(doc! { + "$gt": after + }), + _ => None, + } { + filter.insert("_id", doc); + } + + // 3.2. Execute with given message sort + self.find_with_options( + COL, + filter, + FindOptions::builder() + .limit(limit) + .sort(match sort.unwrap_or(MessageSort::Latest) { + // Sort by relevance, fallback to latest + MessageSort::Relevance => { + if is_search_query { + doc! { + "score": { + "$meta": "textScore" + } + } + } else { + doc! { + "_id": -1_i32 + } + } + } + // Sort by latest first + MessageSort::Latest => doc! { + "_id": -1_i32 + }, + // Sort by oldest first + MessageSort::Oldest => doc! { + "_id": 1_i32 + }, + }) + .build(), + ) + .await + .map_err(|_| create_database_error!("find", COL)) + } + }*/ } - /// Fetch emoji by their parent ids - async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result> { - let emojis = self.emojis.lock().await; - Ok(emojis - .values() - .filter(|emoji| match &emoji.parent { - EmojiParent::Server { id } => parent_ids.contains(id), - _ => false, - }) - .cloned() - .collect()) - } - - /// Detach an emoji by its id - async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { - let mut emojis = self.emojis.lock().await; - if let Some(bot) = emojis.get_mut(&emoji.id) { - bot.parent = EmojiParent::Detached; + /// Update a given message with new information + async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> { + let mut messages = self.messages.lock().await; + if let Some(message_data) = messages.get_mut(id) { + message_data.apply_options(message.to_owned()); Ok(()) } else { Err(create_error!(NotFound)) } } + + /// Append information to a given message + async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> { + let mut messages = self.messages.lock().await; + if let Some(message_data) = messages.get_mut(id) { + if let Some(embeds) = &append.embeds { + if !embeds.is_empty() { + if let Some(embeds_data) = &mut message_data.embeds { + embeds_data.extend(embeds.clone()); + } else { + message_data.embeds = Some(embeds.clone()); + } + } + } + + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Add a new reaction to a message + async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + let mut messages = self.messages.lock().await; + if let Some(message) = messages.get_mut(id) { + if let Some(users) = message.reactions.get_mut(emoji) { + users.insert(user.to_string()); + } else { + message + .reactions + .insert(emoji.to_string(), IndexSet::from([user.to_string()])); + } + + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Remove a reaction from a message + async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { + let mut messages = self.messages.lock().await; + if let Some(message) = messages.get_mut(id) { + if let Some(users) = message.reactions.get_mut(emoji) { + users.remove(&user.to_string()); + } + + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Remove reaction from a message + async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> { + let mut messages = self.messages.lock().await; + if let Some(message) = messages.get_mut(id) { + message.reactions.remove(emoji); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a message from the database by its id + async fn delete_message(&self, id: &str) -> Result<()> { + let mut messages = self.messages.lock().await; + if messages.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete messages from a channel by their ids and corresponding channel id + async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> { + self.messages + .lock() + .await + .retain(|id, message| message.channel != channel && !ids.contains(id)); + + Ok(()) + } } diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs index e5271dfb..b4f6662f 100644 --- a/crates/core/database/src/models/mod.rs +++ b/crates/core/database/src/models/mod.rs @@ -43,6 +43,7 @@ pub trait AbstractDatabase: + channel_webhooks::AbstractWebhooks + emojis::AbstractEmojis + files::AbstractAttachments + + messages::AbstractMessages + ratelimit_events::AbstractRatelimitEvents + server_bans::AbstractServerBans + server_members::AbstractServerMembers diff --git a/crates/core/database/src/models/ratelimit_events/model.rs b/crates/core/database/src/models/ratelimit_events/model.rs index 6ae81217..b25858df 100644 --- a/crates/core/database/src/models/ratelimit_events/model.rs +++ b/crates/core/database/src/models/ratelimit_events/model.rs @@ -1,5 +1,10 @@ use std::fmt; +use revolt_result::Result; +use ulid::Ulid; + +use crate::Database; + auto_derived!( /// Ratelimit Event pub struct RatelimitEvent { @@ -23,3 +28,20 @@ impl fmt::Display for RatelimitEventType { fmt::Debug::fmt(self, f) } } + +#[allow(clippy::disallowed_methods)] +impl RatelimitEvent { + /// Create ratelimit event + pub async fn create( + db: &Database, + target_id: String, + event_type: RatelimitEventType, + ) -> Result<()> { + db.insert_ratelimit_event(&RatelimitEvent { + id: Ulid::new().to_string(), + target_id, + event_type, + }) + .await + } +} diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs index 629ffac4..dc86cf5e 100644 --- a/crates/core/database/src/models/server_members/model.rs +++ b/crates/core/database/src/models/server_members/model.rs @@ -1,7 +1,11 @@ use iso8601_timestamp::Timestamp; -use revolt_result::Result; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; -use crate::{Database, File, Server}; +use crate::{ + events::client::EventV1, util::permissions::DatabasePermissionQuery, Database, File, Server, + SystemMessage, User, +}; auto_derived_partial!( /// Server Member @@ -11,8 +15,7 @@ auto_derived_partial!( pub id: MemberCompositeKey, /// Time at which this user joined the server - #[serde(skip_serializing_if = "Option::is_none")] - pub joined_at: Option, + pub joined_at: Timestamp, /// Member's nickname #[serde(skip_serializing_if = "Option::is_none")] @@ -57,7 +60,102 @@ auto_derived!( } ); +impl Default for Member { + fn default() -> Self { + Self { + id: Default::default(), + joined_at: Timestamp::now_utc(), + nickname: None, + avatar: None, + roles: vec![], + timeout: None, + } + } +} + +#[allow(clippy::disallowed_methods)] impl Member { + /// Create a new member in a server + pub async fn create( + db: &Database, + server: &Server, + user: &User, + // channels: Option>, + //) -> Result> { + ) -> Result<()> { + if db.fetch_ban(&server.id, &user.id).await.is_ok() { + return Err(create_error!(Banned)); + } + + if db.fetch_member(&server.id, &user.id).await.is_ok() { + return Err(create_error!(AlreadyInServer)); + } + + let member = Member { + id: MemberCompositeKey { + server: server.id.to_string(), + user: user.id.to_string(), + }, + ..Default::default() + }; + + db.insert_member(&member).await?; + + let mut channels = vec![]; + + if true { + let query = DatabasePermissionQuery::new(db, user).server(server); + let existing_channels = db.fetch_channels(&server.channels).await?; + + for channel in existing_channels { + let mut channel_query = query.clone().channel(&channel); + + if calculate_channel_permissions(&mut channel_query) + .await + .has_channel_permission(ChannelPermission::ViewChannel) + { + channels.push(channel); + } + } + } + + EventV1::ServerMemberJoin { + id: server.id.clone(), + user: user.id.clone(), + } + .p(server.id.clone()) + .await; + + EventV1::ServerCreate { + id: server.id.clone(), + server: server.clone().into(), + channels: channels + .clone() + .into_iter() + .map(|channel| channel.into()) + .collect(), + } + .private(user.id.clone()) + .await; + + if let Some(id) = server + .system_messages + .as_ref() + .and_then(|x| x.user_joined.as_ref()) + { + SystemMessage::UserJoined { + id: user.id.clone(), + } + .into_message(id.to_string()) + .send_without_notifications(db, false, false) + .await + .ok(); + } + + // Ok(channels) + Ok(()) + } + /// Update member data pub async fn update<'a>( &mut self, @@ -73,13 +171,13 @@ impl Member { db.update_member(&self.id, &partial, remove.clone()).await?; - /* // TODO: EventV1::ServerMemberUpdate { - id: self.id.clone(), - data: partial, - clear: remove, + EventV1::ServerMemberUpdate { + id: self.id.clone().into(), + data: partial.into(), + clear: remove.into_iter().map(|field| field.into()).collect(), } .p(self.id.server.clone()) - .await; */ + .await; Ok(()) } diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index d3c48f4d..13074ba5 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -128,6 +128,7 @@ auto_derived!( } ); +#[allow(clippy::disallowed_methods)] impl Server { /// Create a server pub async fn create(&self, db: &Database) -> Result<()> { diff --git a/crates/core/database/src/models/users/mod.rs b/crates/core/database/src/models/users/mod.rs index 04963173..84935c6f 100644 --- a/crates/core/database/src/models/users/mod.rs +++ b/crates/core/database/src/models/users/mod.rs @@ -2,8 +2,12 @@ mod model; mod ops; #[cfg(feature = "rocket-impl")] mod rocket; +#[cfg(feature = "rocket-impl")] +mod schema; #[cfg(feature = "rocket-impl")] pub use self::rocket::*; +#[cfg(feature = "rocket-impl")] +pub use self::schema::*; pub use model::*; pub use ops::*; diff --git a/crates/core/database/src/models/users/model.rs b/crates/core/database/src/models/users/model.rs index 1e49e022..c32e374e 100644 --- a/crates/core/database/src/models/users/model.rs +++ b/crates/core/database/src/models/users/model.rs @@ -1,9 +1,11 @@ -use std::collections::HashSet; +use std::{collections::HashSet, time::Duration}; -use crate::{Database, File}; +use crate::{events::client::EventV1, Database, File, RatelimitEvent}; use once_cell::sync::Lazy; -use revolt_result::{Error, ErrorType, Result}; +use rand::seq::SliceRandom; +use revolt_result::{create_error, Error, ErrorType, Result}; +use ulid::Ulid; auto_derived_partial!( /// # User @@ -49,6 +51,15 @@ auto_derived_partial!( ); auto_derived!( + /// Optional fields on user object + pub enum FieldsUser { + Avatar, + StatusText, + StatusPresence, + ProfileContent, + ProfileBackground, + } + /// User's relationship with another user (or themselves) pub enum RelationshipStatus { None, @@ -106,18 +117,202 @@ auto_derived!( /// Id of the owner of this bot pub owner: String, } - - /// Optional fields on user object - pub enum FieldsUser { - Avatar, - StatusText, - StatusPresence, - ProfileContent, - ProfileBackground, - } ); +pub static DISCRIMINATOR_SEARCH_SPACE: Lazy> = Lazy::new(|| { + let mut set = (2..9999) + .map(|v| format!("{:0>4}", v)) + .collect::>(); + + for discrim in [ + 123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999, + ] { + set.remove(&format!("{:0>4}", discrim)); + } + + set.into_iter().collect() +}); + +#[allow(clippy::derivable_impls)] +impl Default for User { + fn default() -> Self { + Self { + id: Default::default(), + username: Default::default(), + discriminator: Default::default(), + display_name: Default::default(), + avatar: Default::default(), + relations: Default::default(), + badges: Default::default(), + status: Default::default(), + profile: Default::default(), + flags: Default::default(), + privileged: Default::default(), + bot: Default::default(), + } + } +} + +#[allow(clippy::disallowed_methods)] impl User { + /// Create a new user + pub async fn create( + db: &Database, + username: String, + account_id: I, + data: D, + ) -> Result + where + I: Into>, + D: Into>, + { + let username = User::validate_username(username)?; + let mut user = User { + id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()), + discriminator: User::find_discriminator(db, &username, None).await?, + username, + ..Default::default() + }; + + if let Some(data) = data.into() { + user.apply_options(data); + } + + db.insert_user(&user).await?; + Ok(user) + } + + /// Check whether two users have a mutual connection + /// + /// This will check if user and user_b share a server or a group. + pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result { + Ok(!db + .fetch_mutual_server_ids(&self.id, user_b) + .await? + .is_empty() + || !db + .fetch_mutual_channel_ids(&self.id, user_b) + .await? + .is_empty()) + } + + /// Sanitise and validate a username can be used + pub fn validate_username(username: String) -> Result { + // Copy the username for validation + let username_lowercase = username.to_lowercase(); + + // Block homoglyphs + if decancer::cure(&username_lowercase).into_str() != username_lowercase { + return Err(create_error!(InvalidUsername)); + } + + // Ensure the username itself isn't blocked + const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"]; + + for username in BLOCKED_USERNAMES { + if username_lowercase == *username { + return Err(create_error!(InvalidUsername)); + } + } + + // Ensure none of the following substrings show up in the username + const BLOCKED_SUBSTRINGS: &[&str] = &["```"]; + + for substr in BLOCKED_SUBSTRINGS { + if username_lowercase.contains(substr) { + return Err(create_error!(InvalidUsername)); + } + } + + Ok(username) + } + + // Find a free discriminator for a given username + pub async fn find_discriminator( + db: &Database, + username: &str, + preferred: Option<(String, String)>, + ) -> Result { + let search_space: &HashSet = &DISCRIMINATOR_SEARCH_SPACE; + let used_discriminators: HashSet = db + .fetch_discriminators_in_use(username) + .await? + .into_iter() + .collect(); + + let available_discriminators: Vec<&String> = + search_space.difference(&used_discriminators).collect(); + + if available_discriminators.is_empty() { + return Err(create_error!(UsernameTaken)); + } + + if let Some((preferred, target_id)) = preferred { + if available_discriminators.contains(&&preferred) { + return Ok(preferred); + } else { + if db + .has_ratelimited( + &target_id, + crate::RatelimitEventType::DiscriminatorChange, + Duration::from_secs(60 * 60 * 24), + 1, + ) + .await? + { + return Err(create_error!(DiscriminatorChangeRatelimited)); + } + + RatelimitEvent::create( + db, + target_id, + crate::RatelimitEventType::DiscriminatorChange, + ) + .await?; + } + } + + let mut rng = rand::thread_rng(); + Ok(available_discriminators + .choose(&mut rng) + .expect("we can assert this has an element") + .to_string()) + } + + /// Update a user's username + pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> { + let username = User::validate_username(username)?; + if self.username.to_lowercase() == username.to_lowercase() { + self.update( + db, + PartialUser { + username: Some(username), + ..Default::default() + }, + vec![], + ) + .await + } else { + self.update( + db, + PartialUser { + discriminator: Some( + User::find_discriminator( + db, + &username, + Some((self.discriminator.to_string(), self.id.clone())), + ) + .await?, + ), + username: Some(username), + ..Default::default() + }, + vec![], + ) + .await + } + } + /// Check whether a username is already in use by another user #[allow(dead_code)] async fn is_username_taken(db: &Database, username: &str) -> Result { @@ -145,13 +340,14 @@ impl User { self.apply_options(partial.clone()); db.update_user(&self.id, &partial, remove.clone()).await?; - /* // TODO: EventV1::UserUpdate { + EventV1::UserUpdate { id: self.id.clone(), - data: partial, - clear: remove, + data: partial.into(), + clear: remove.into_iter().map(|v| v.into()).collect(), + event_id: Some(Ulid::new().to_string()), } .p_user(self.id.clone(), db) - .await; */ + .await; Ok(()) } @@ -203,17 +399,3 @@ impl User { .await } } - -pub static DISCRIMINATOR_SEARCH_SPACE: Lazy> = Lazy::new(|| { - let mut set = (2..9999) - .map(|v| format!("{:0>4}", v)) - .collect::>(); - - for discrim in [ - 123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999, - ] { - set.remove(&format!("{:0>4}", discrim)); - } - - set.into_iter().collect() -}); diff --git a/crates/core/database/src/models/users/ops.rs b/crates/core/database/src/models/users/ops.rs index 28421ec0..a07ceb87 100644 --- a/crates/core/database/src/models/users/ops.rs +++ b/crates/core/database/src/models/users/ops.rs @@ -22,6 +22,9 @@ pub trait AbstractUsers: Sync + Send { /// Fetch multiple users by their ids async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result>; + /// Fetch all discriminators in use for a username + async fn fetch_discriminators_in_use(&self, username: &str) -> Result>; + /// Fetch ids of users that both users are friends with async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result>; diff --git a/crates/core/database/src/models/users/ops/mongodb.rs b/crates/core/database/src/models/users/ops/mongodb.rs index 6bea5077..9212c806 100644 --- a/crates/core/database/src/models/users/ops/mongodb.rs +++ b/crates/core/database/src/models/users/ops/mongodb.rs @@ -87,6 +87,39 @@ impl AbstractUsers for MongoDb { .await) } + /// Fetch all discriminators in use for a username + async fn fetch_discriminators_in_use(&self, username: &str) -> Result> { + #[derive(Deserialize)] + struct UserDocument { + discriminator: String, + } + + Ok(self + .col::(COL) + .find( + doc! { + "username": username + }, + FindOptions::builder() + .collation( + Collation::builder() + .locale("en") + .strength(CollationStrength::Secondary) + .build(), + ) + .projection(doc! { "_id": 0, "discriminator": 1 }) + .build(), + ) + .await + .map_err(|_| create_database_error!("find", COL))? + .filter_map(|s| async { s.ok() }) + .collect::>() + .await + .into_iter() + .map(|user| user.discriminator) + .collect::>()) + } + /// Fetch ids of users that both users are friends with async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result> { Ok(self diff --git a/crates/core/database/src/models/users/ops/reference.rs b/crates/core/database/src/models/users/ops/reference.rs index d75af7a6..c557bea5 100644 --- a/crates/core/database/src/models/users/ops/reference.rs +++ b/crates/core/database/src/models/users/ops/reference.rs @@ -56,6 +56,18 @@ impl AbstractUsers for ReferenceDb { .collect() } + /// Fetch all discriminators in use for a username + async fn fetch_discriminators_in_use(&self, username: &str) -> Result> { + let users = self.users.lock().await; + let lowercase = username.to_lowercase(); + Ok(users + .values() + .filter(|user| user.username.to_lowercase() == lowercase) + .map(|user| &user.discriminator) + .cloned() + .collect()) + } + /// Fetch ids of users that both users are friends with async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result> { todo!() diff --git a/crates/core/database/src/models/users/schema.rs b/crates/core/database/src/models/users/schema.rs new file mode 100644 index 00000000..d887b895 --- /dev/null +++ b/crates/core/database/src/models/users/schema.rs @@ -0,0 +1,31 @@ +use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData}; +use revolt_rocket_okapi::{ + gen::OpenApiGenerator, + request::{OpenApiFromRequest, RequestHeaderInput}, +}; + +use crate::User; + +impl<'r> OpenApiFromRequest<'r> for User { + fn from_request_input( + _gen: &mut OpenApiGenerator, + _name: String, + _required: bool, + ) -> revolt_rocket_okapi::Result { + let mut requirements = schemars::Map::new(); + requirements.insert("Session Token".to_owned(), vec![]); + + Ok(RequestHeaderInput::Security( + "Session Token".to_owned(), + SecurityScheme { + data: SecuritySchemeData::ApiKey { + name: "x-session-token".to_owned(), + location: "header".to_owned(), + }, + description: Some("Used to authenticate as a user.".to_owned()), + extensions: schemars::Map::new(), + }, + requirements, + )) + } +} diff --git a/crates/core/database/src/tasks/ack.rs b/crates/core/database/src/tasks/ack.rs new file mode 100644 index 00000000..c055fb4a --- /dev/null +++ b/crates/core/database/src/tasks/ack.rs @@ -0,0 +1,122 @@ +// Queue Type: Debounced +use crate::Database; + +use deadqueue::limited::Queue; +use once_cell::sync::Lazy; +use std::{collections::HashMap, time::Duration}; + +use super::DelayedTask; + +/// Enumeration of possible events +#[derive(Debug, Eq, PartialEq)] +pub enum AckEvent { + /// Add mentions for a user in a channel + AddMention { + /// Message IDs + ids: Vec, + }, + + /// Acknowledge message in a channel for a user + AckMessage { + /// Message ID + id: String, + }, +} + +/// Task information +struct Data { + /// Channel to ack + channel: String, + /// User to ack for + user: String, + /// Event + event: AckEvent, +} + +#[derive(Debug)] +struct Task { + event: AckEvent, +} + +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + +/// Queue a new task for a worker +pub async fn queue(channel: String, user: String, event: AckEvent) { + Q.try_push(Data { + channel, + user, + event, + }) + .ok(); + + info!("Queue is using {} slots from {}.", Q.len(), Q.capacity()); +} + +/// Start a new worker +pub async fn worker(db: Database) { + let mut tasks = HashMap::<(String, String), DelayedTask>::new(); + let mut keys = vec![]; + + loop { + // Find due tasks. + for (key, task) in &tasks { + if task.should_run() { + keys.push(key.clone()); + } + } + + // Commit any due tasks to the database. + for key in &keys { + if let Some(task) = tasks.remove(key) { + let Task { event } = task.data; + let (user, channel) = key; + + if let Err(err) = match &event { + #[allow(clippy::disallowed_methods)] // event is sent by higher level function + AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await, + AckEvent::AddMention { ids } => { + db.add_mention_to_unread(channel, user, ids).await + } + } { + error!("{err:?} for {event:?}. ({user}, {channel})"); + } else { + info!("User {user} ack in {channel} with {event:?}"); + } + } + } + + // Clear keys + keys.clear(); + + // Queue incoming tasks. + while let Some(Data { + channel, + user, + mut event, + }) = Q.try_pop() + { + let key = (user, channel); + if let Some(task) = tasks.get_mut(&key) { + task.delay(); + + match &mut event { + AckEvent::AddMention { ids } => { + if let AckEvent::AddMention { ids: existing } = &mut task.data.event { + existing.append(ids); + } else { + task.data.event = event; + } + } + AckEvent::AckMessage { .. } => { + task.data.event = event; + } + } + } else { + tasks.insert(key, DelayedTask::new(Task { event })); + } + } + + // Sleep for an arbitrary amount of time. + async_std::task::sleep(Duration::from_secs(1)).await; + } +} diff --git a/crates/core/database/src/tasks/last_message_id.rs b/crates/core/database/src/tasks/last_message_id.rs new file mode 100644 index 00000000..94c14c17 --- /dev/null +++ b/crates/core/database/src/tasks/last_message_id.rs @@ -0,0 +1,87 @@ +// Queue Type: Debounced +use deadqueue::limited::Queue; +use once_cell::sync::Lazy; +use std::{collections::HashMap, time::Duration}; + +use crate::{Database, PartialChannel}; + +use super::DelayedTask; + +/// Task information +struct Data { + /// Channel to update + channel: String, + /// Latest message ID + id: String, + /// Whether the channel is a DM + is_dm: bool, +} + +/// Task information +#[derive(Debug)] +struct Task { + /// Latest message ID + id: String, + /// Whether the channel is a DM + is_dm: bool, +} + +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + +/// Queue a new task for a worker +pub async fn queue(channel: String, id: String, is_dm: bool) { + Q.try_push(Data { channel, id, is_dm }).ok(); + info!("Queue is using {} slots from {}.", Q.len(), Q.capacity()); +} + +/// Start a new worker +pub async fn worker(db: Database) { + let mut tasks = HashMap::>::new(); + let mut keys = vec![]; + + loop { + // Find due tasks. + for (key, task) in &tasks { + if task.should_run() { + keys.push(key.clone()); + } + } + + // Commit any due tasks to the database. + for key in &keys { + if let Some(task) = tasks.remove(key) { + let Task { id, is_dm, .. } = task.data; + + let mut channel = PartialChannel { + last_message_id: Some(id.to_string()), + ..Default::default() + }; + + if is_dm { + channel.active = Some(true); + } + + match db.update_channel(key, &channel, vec![]).await { + Ok(_) => info!("Updated last_message_id for {key} to {id}."), + Err(err) => error!("Failed to update last_message_id with {err:?}!"), + } + } + } + + // Clear keys + keys.clear(); + + // Queue incoming tasks. + while let Some(Data { channel, id, is_dm }) = Q.try_pop() { + if let Some(task) = tasks.get_mut(&channel) { + task.data.id = id; + task.delay(); + } else { + tasks.insert(channel, DelayedTask::new(Task { id, is_dm })); + } + } + + // Sleep for an arbitrary amount of time. + async_std::task::sleep(Duration::from_secs(1)).await; + } +} diff --git a/crates/core/database/src/tasks/mod.rs b/crates/core/database/src/tasks/mod.rs new file mode 100644 index 00000000..93f15a5a --- /dev/null +++ b/crates/core/database/src/tasks/mod.rs @@ -0,0 +1,58 @@ +//! Semi-important background task management + +use crate::Database; + +use async_std::task; +use std::time::Instant; + +const WORKER_COUNT: usize = 5; + +pub mod ack; +pub mod last_message_id; +pub mod process_embeds; +pub mod web_push; + +/// Spawn background workers +pub async fn start_workers(db: Database) { + for _ in 0..WORKER_COUNT { + task::spawn(ack::worker(db.clone())); + task::spawn(last_message_id::worker(db.clone())); + task::spawn(process_embeds::worker(db.clone())); + task::spawn(web_push::worker(db.clone().into())); + } +} + +/// Task with additional information on when it should run +pub struct DelayedTask { + pub data: T, + last_updated: Instant, + first_seen: Instant, +} + +/// Commit to database every 30 seconds if the task is particularly active. +static EXPIRE_CONSTANT: u64 = 30; + +/// Otherwise, commit to database after 5 seconds. +static SAVE_CONSTANT: u64 = 5; + +impl DelayedTask { + /// Create a new delayed task + pub fn new(data: T) -> Self { + DelayedTask { + data, + last_updated: Instant::now(), + first_seen: Instant::now(), + } + } + + /// Push a task further back in time + pub fn delay(&mut self) { + self.last_updated = Instant::now() + } + + /// Check if a task should run yet + pub fn should_run(&self) -> bool { + self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT + || self.last_updated.elapsed().as_secs() > SAVE_CONSTANT + } +} diff --git a/crates/core/database/src/tasks/process_embeds.rs b/crates/core/database/src/tasks/process_embeds.rs new file mode 100644 index 00000000..e03ba914 --- /dev/null +++ b/crates/core/database/src/tasks/process_embeds.rs @@ -0,0 +1,170 @@ +use crate::{models::Message, AppendMessage, Database}; + +use futures::future::join_all; +use linkify::{LinkFinder, LinkKind}; +use regex::Regex; +use revolt_config::config; +use revolt_result::Result; + +use async_lock::Semaphore; +use async_std::task::spawn; +use deadqueue::limited::Queue; +use once_cell::sync::Lazy; +use revolt_models::v0::Embed; +use std::{collections::HashSet, sync::Arc}; + +use isahc::prelude::*; + +/// Task information +#[derive(Debug)] +struct EmbedTask { + /// Channel we're processing the event in + channel: String, + /// ID of the message we're processing + id: String, + /// Content of the message + content: String, +} + +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + +/// Queue a new task for a worker +pub async fn queue(channel: String, id: String, content: String) { + Q.try_push(EmbedTask { + channel, + id, + content, + }) + .ok(); + + info!("Queue is using {} slots from {}.", Q.len(), Q.capacity()); +} + +/// Start a new worker +pub async fn worker(db: Database) { + let semaphore = Arc::new(Semaphore::new( + config().await.api.workers.max_concurrent_connections, + )); + + loop { + let task = Q.pop().await; + let db = db.clone(); + let semaphore = semaphore.clone(); + + spawn(async move { + let config = config().await; + let embeds = generate( + task.content, + &config.hosts.january, + config.features.limits.default.message_embeds, + semaphore, + ) + .await; + + if let Ok(embeds) = embeds { + if let Err(err) = Message::append( + &db, + task.id, + task.channel, + AppendMessage { + embeds: Some(embeds), + }, + ) + .await + { + error!("Encountered an error appending to message: {:?}", err); + } + } + }); + } +} + +static RE_CODE: Lazy = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap()); +static RE_IGNORED: Lazy = Lazy::new(|| Regex::new("()").unwrap()); + +pub async fn generate( + content: String, + host: &str, + max_embeds: usize, + semaphore: Arc, +) -> Result> { + // Ignore code blocks. + let content = RE_CODE.replace_all(&content, ""); + + // Ignore all content between angle brackets starting with http. + let content = RE_IGNORED.replace_all(&content, ""); + + let content = content + // Ignore quoted lines. + .split('\n') + .map(|v| { + if let Some(c) = v.chars().next() { + if c == '>' { + return ""; + } + } + + v + }) + .collect::>() + .join("\n"); + + let mut finder = LinkFinder::new(); + finder.kinds(&[LinkKind::Url]); + + // Process all links, stripping anchors and + // only taking up to `max_embeds` of links. + let links: Vec = finder + .links(&content) + .map(|x| { + x.as_str() + .chars() + .take_while(|&ch| ch != '#') + .collect::() + }) + .collect::>() + .into_iter() + .take(max_embeds) + .collect(); + + // If no links, fail out. + if links.is_empty() { + return Err(create_error!(LabelMe)); + } + + // ! FIXME: batch request to january + let mut tasks = Vec::new(); + + for link in links { + let semaphore = semaphore.clone(); + let host = host.to_string(); + tasks.push(spawn(async move { + let guard = semaphore.acquire().await; + + if let Ok(mut response) = isahc::get_async(format!( + "{host}/embed?url={}", + url_escape::encode_component(&link) + )) + .await + { + drop(guard); + response.json::().await.ok() + } else { + None + } + })); + } + + let embeds = join_all(tasks) + .await + .into_iter() + .flatten() + .collect::>(); + + // Prevent database update when no embeds are found. + if !embeds.is_empty() { + Ok(embeds) + } else { + Err(create_error!(LabelMe)) + } +} diff --git a/crates/core/database/src/tasks/web_push.rs b/crates/core/database/src/tasks/web_push.rs new file mode 100644 index 00000000..8a055afe --- /dev/null +++ b/crates/core/database/src/tasks/web_push.rs @@ -0,0 +1,162 @@ +use std::collections::HashSet; + +use authifier::Database; +use base64::{ + engine::{self}, + Engine as _, +}; +use deadqueue::limited::Queue; +use once_cell::sync::Lazy; +use revolt_config::config; +use revolt_models::v0::PushNotification; +use revolt_presence::filter_online; +use serde_json::json; +use web_push::{ + ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, + WebPushClient, WebPushMessageBuilder, +}; + +/// Task information +#[derive(Debug)] +struct PushTask { + /// User IDs of the targets that are to receive this notification + recipients: Vec, + /// Push Notification + payload: PushNotification, +} + +static Q: Lazy> = Lazy::new(|| Queue::new(10_000)); + +/// Queue a new task for a worker +pub async fn queue(recipients: Vec, payload: PushNotification) { + if recipients.is_empty() { + return; + } + + let online_ids = filter_online(&recipients).await; + let recipients = (&recipients.into_iter().collect::>() - &online_ids) + .into_iter() + .collect::>(); + + Q.try_push(PushTask { + recipients, + payload, + }) + .ok(); + + info!("Queue is using {} slots from {}.", Q.len(), Q.capacity()); +} + +/// Start a new worker +pub async fn worker(db: Database) { + let config = config().await; + + let web_push_client = IsahcWebPushClient::new().unwrap(); + let fcm_client = if config.api.fcm.api_key.is_empty() { + None + } else { + Some(fcm::Client::new()) + }; + + let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD + .decode(config.api.vapid.private_key) + .expect("valid `VAPID_PRIVATE_KEY`"); + + loop { + let task = Q.pop().await; + + if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await { + for session in sessions { + if let Some(sub) = session.subscription { + if sub.endpoint == "fcm" { + // Use Firebase Cloud Messaging + if let Some(client) = &fcm_client { + let PushNotification { + author, + icon, + image: _, + body, + tag, + timestamp: _, + url: _, + } = &task.payload; + + let mut notification = fcm::NotificationBuilder::new(); + notification.title(author); + notification.icon(icon); + notification.body(body); + notification.tag(tag); + // TODO: expand support for fields + let notification = notification.finalize(); + + let mut message_builder = + fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth); + message_builder.notification(notification); + + if let Err(err) = client.send(message_builder.finalize()).await { + error!("Failed to send FCM notification! {:?}", err); + } else { + info!("Sent FCM notification to {:?}.", session.id); + } + } else { + info!("No FCM token was specified!"); + } + } else { + // Use Web Push Standard + let subscription = SubscriptionInfo { + endpoint: sub.endpoint, + keys: SubscriptionKeys { + auth: sub.auth, + p256dh: sub.p256dh, + }, + }; + + match VapidSignatureBuilder::from_pem( + std::io::Cursor::new(&web_push_private_key), + &subscription, + ) { + Ok(sig_builder) => match sig_builder.build() { + Ok(signature) => { + let mut builder = WebPushMessageBuilder::new(&subscription); + builder.set_vapid_signature(signature); + + let payload = json!(task.payload).to_string(); + builder + .set_payload(ContentEncoding::AesGcm, payload.as_bytes()); + + match builder.build() { + Ok(msg) => match web_push_client.send(msg).await { + Ok(_) => { + info!( + "Sent Web Push notification to {:?}.", + session.id + ) + } + Err(err) => { + error!("Hit error sending Web Push! {:?}", err) + } + }, + Err(err) => { + error!( + "Failed to build message for {}! {:?}", + session.user_id, err + ) + } + } + } + Err(err) => error!( + "Failed to build signature for {}! {:?}", + session.user_id, err + ), + }, + Err(err) => error!( + "Failed to create signature builder for {}! {:?}", + session.user_id, err + ), + } + } + } + } + } + } +} diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index f329e345..c60fee27 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -34,6 +34,24 @@ impl From for Bot { } } +impl From for crate::FieldsBot { + fn from(value: FieldsBot) -> Self { + match value { + FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL, + FieldsBot::Token => crate::FieldsBot::Token, + } + } +} + +impl From for FieldsBot { + fn from(value: crate::FieldsBot) -> Self { + match value { + crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL, + crate::FieldsBot::Token => FieldsBot::Token, + } + } +} + impl From for Invite { fn from(value: crate::Invite) -> Self { match value { @@ -373,6 +391,14 @@ impl From for Interactions { } } +impl From for AppendMessage { + fn from(value: crate::AppendMessage) -> Self { + AppendMessage { + embeds: value.embeds, + } + } +} + impl From for Masquerade { fn from(value: crate::Masquerade) -> Self { Masquerade { @@ -605,6 +631,83 @@ impl crate::User { id: self.id, } } + + pub async fn into_self(self) -> User { + User { + username: self.username, + discriminator: self.discriminator, + display_name: self.display_name, + avatar: self.avatar.map(|file| file.into()), + relations: self + .relations + .map(|relationships| { + relationships + .into_iter() + .map(|relationship| relationship.into()) + .collect() + }) + .unwrap_or_default(), + badges: self.badges.unwrap_or_default() as u32, + status: self.status.map(|status| status.into()), + profile: self.profile.map(|profile| profile.into()), + flags: self.flags.unwrap_or_default() as u32, + privileged: self.privileged, + bot: self.bot.map(|bot| bot.into()), + relationship: RelationshipStatus::User, + online: revolt_presence::is_online(&self.id).await, + id: self.id, + } + } +} + +impl From for PartialUser { + fn from(value: crate::PartialUser) -> Self { + PartialUser { + username: value.username, + discriminator: value.discriminator, + display_name: value.display_name, + avatar: value.avatar.map(|file| file.into()), + relations: value.relations.map(|relationships| { + relationships + .into_iter() + .map(|relationship| relationship.into()) + .collect() + }), + badges: value.badges.map(|badges| badges as u32), + status: value.status.map(|status| status.into()), + profile: value.profile.map(|profile| profile.into()), + flags: value.flags.map(|flags| flags as u32), + privileged: value.privileged, + bot: value.bot.map(|bot| bot.into()), + relationship: None, + online: None, + id: value.id, + } + } +} + +impl From for crate::FieldsUser { + fn from(value: FieldsUser) -> Self { + match value { + FieldsUser::Avatar => crate::FieldsUser::Avatar, + FieldsUser::ProfileBackground => crate::FieldsUser::ProfileBackground, + FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent, + FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence, + FieldsUser::StatusText => crate::FieldsUser::StatusText, + } + } +} + +impl From for FieldsUser { + fn from(value: crate::FieldsUser) -> Self { + match value { + crate::FieldsUser::Avatar => FieldsUser::Avatar, + crate::FieldsUser::ProfileBackground => FieldsUser::ProfileBackground, + crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent, + crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence, + crate::FieldsUser::StatusText => FieldsUser::StatusText, + } + } } impl From for RelationshipStatus { diff --git a/crates/quark/src/web/idempotency.rs b/crates/core/database/src/util/idempotency.rs similarity index 79% rename from crates/quark/src/web/idempotency.rs rename to crates/core/database/src/util/idempotency.rs index 49004a3a..1db20993 100644 --- a/crates/quark/src/web/idempotency.rs +++ b/crates/core/database/src/util/idempotency.rs @@ -1,6 +1,9 @@ -use crate::{Error, Result}; +use std::num::NonZeroUsize; + +use revolt_result::{create_error, Error, Result}; use async_std::sync::Mutex; +use once_cell::sync::Lazy; use revolt_rocket_okapi::gen::OpenApiGenerator; use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue}; @@ -8,16 +11,14 @@ use rocket::http::Status; use rocket::request::{FromRequest, Outcome}; use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use serde::{Deserialize, Serialize}; -use validator::Validate; -use once_cell::sync::Lazy; -#[derive(Validate, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct IdempotencyKey { - #[validate(length(min = 1, max = 64))] key: String, } -static TOKEN_CACHE: Lazy>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); +static TOKEN_CACHE: Lazy>> = + Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap()))); impl IdempotencyKey { // Backwards compatibility. @@ -26,7 +27,7 @@ impl IdempotencyKey { if let Some(v) = v { let mut cache = TOKEN_CACHE.lock().await; if cache.get(&v).is_some() { - return Err(Error::DuplicateNonce); + return Err(create_error!(DuplicateNonce)); } cache.put(v.clone(), ()); @@ -81,14 +82,19 @@ impl<'r> FromRequest<'r> for IdempotencyKey { .next() .map(|k| k.to_string()) { - let idempotency = IdempotencyKey { key }; - if let Err(error) = idempotency.validate() { - return Outcome::Failure((Status::BadRequest, Error::FailedValidation { error })); + if key.len() > 64 { + return Outcome::Failure(( + Status::BadRequest, + create_error!(FailedValidation { + error: "idempotency key too long".to_string(), + }), + )); } + let idempotency = IdempotencyKey { key }; let mut cache = TOKEN_CACHE.lock().await; if cache.get(&idempotency.key).is_some() { - return Outcome::Failure((Status::Conflict, Error::DuplicateNonce)); + return Outcome::Failure((Status::Conflict, create_error!(DuplicateNonce))); } cache.put(idempotency.key.clone(), ()); diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index e7efee98..8f4f4c76 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod bridge; +pub mod idempotency; pub mod permissions; pub mod reference; diff --git a/crates/core/database/src/util/permissions.rs b/crates/core/database/src/util/permissions.rs index c4e0d695..d9be51b8 100644 --- a/crates/core/database/src/util/permissions.rs +++ b/crates/core/database/src/util/permissions.rs @@ -4,18 +4,19 @@ use revolt_permissions::{ calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus, }; -use crate::{Database, User}; +use crate::{Channel, Database, Member, Server, User}; /// Permissions calculator -pub struct PermissionCalculator<'a> { +#[derive(Clone)] +pub struct DatabasePermissionQuery<'a> { #[allow(dead_code)] database: &'a Database, perspective: &'a User, user: Option>, - // pub channel: Cow<'a, Channel>, - // pub server: Cow<'a, Server>, - // pub member: Cow<'a, Member>, + channel: Option>, + server: Option>, + member: Option>, // flag_known_relationship: Option<&'a RelationshipStatus>, cached_user_permission: Option, @@ -23,7 +24,7 @@ pub struct PermissionCalculator<'a> { } #[async_trait] -impl PermissionQuery for PermissionCalculator<'_> { +impl PermissionQuery for DatabasePermissionQuery<'_> { // * For calculating user permission /// Is our perspective user privileged? @@ -81,85 +82,275 @@ impl PermissionQuery for PermissionCalculator<'_> { /// Do we have a mutual connection with the currently selected user? async fn have_mutual_connection(&mut self) -> bool { - // TODO: User::has_mutual_connection - false + if let Some(user) = &self.user { + // TODO: cache result? + matches!( + self.perspective + .has_mutual_connection(self.database, &user.id) + .await, + Ok(true) + ) + } else { + false + } } // * For calculating server permission /// Is our perspective user the server's owner? async fn are_we_server_owner(&mut self) -> bool { - todo!() + if let Some(server) = &self.server { + server.owner == self.perspective.id + } else { + false + } } /// Is our perspective user a member of the server? async fn are_we_a_member(&mut self) -> bool { - todo!() + if let Some(server) = &self.server { + if self.member.is_some() { + true + } else { + self.database + .fetch_member(&server.id, &self.perspective.id) + .await + .is_ok() + } + } else { + false + } } /// Get default server permission async fn get_default_server_permissions(&mut self) -> u64 { - todo!() + if let Some(server) = &self.server { + server.default_permissions as u64 + } else { + 0 + } } /// Get the ordered role overrides (from lowest to highest) for this member in this server async fn get_our_server_role_overrides(&mut self) -> Vec { - todo!() + if let Some(server) = &self.server { + let member_roles = self + .member + .as_ref() + .map(|member| member.roles.clone()) + .unwrap_or_default(); + + let mut roles = server + .roles + .iter() + .filter(|(id, _)| member_roles.contains(id)) + .map(|(_, role)| { + let v: Override = role.permissions.into(); + (role.rank, v) + }) + .collect::>(); + + roles.sort_by(|a, b| b.0.cmp(&a.0)); + roles.into_iter().map(|(_, v)| v).collect() + } else { + vec![] + } } /// Is our perspective user timed out on this server? async fn are_we_timed_out(&mut self) -> bool { - todo!() + if let Some(member) = &self.member { + member.in_timeout() + } else { + false + } } // * For calculating channel permission /// Get the type of the channel async fn get_channel_type(&mut self) -> ChannelType { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::DirectMessage { .. }) + | Cow::Owned(Channel::DirectMessage { .. }) => ChannelType::DirectMessage, + Cow::Borrowed(Channel::Group { .. }) | Cow::Owned(Channel::Group { .. }) => { + ChannelType::Group + } + Cow::Borrowed(Channel::SavedMessages { .. }) + | Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages, + Cow::Borrowed(Channel::TextChannel { .. }) + | Cow::Owned(Channel::TextChannel { .. }) + | Cow::Borrowed(Channel::VoiceChannel { .. }) + | Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel, + } + } else { + ChannelType::Unknown + } } /// Get the default channel permissions /// Group channel defaults should be mapped to an allow-only override async fn get_default_channel_permissions(&mut self) -> Override { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::Group { permissions, .. }) + | Cow::Owned(Channel::Group { permissions, .. }) => Override { + allow: permissions.unwrap_or_default() as u64, + deny: 0, + }, + Cow::Borrowed(Channel::TextChannel { + default_permissions, + .. + }) + | Cow::Owned(Channel::TextChannel { + default_permissions, + .. + }) + | Cow::Borrowed(Channel::VoiceChannel { + default_permissions, + .. + }) + | Cow::Owned(Channel::VoiceChannel { + default_permissions, + .. + }) => default_permissions.unwrap_or_default().into(), + _ => Default::default(), + } + } else { + Default::default() + } } /// Get the ordered role overrides (from lowest to highest) for this member in this channel async fn get_our_channel_role_overrides(&mut self) -> Vec { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::TextChannel { + role_permissions, .. + }) + | Cow::Owned(Channel::TextChannel { + role_permissions, .. + }) + | Cow::Borrowed(Channel::VoiceChannel { + role_permissions, .. + }) + | Cow::Owned(Channel::VoiceChannel { + role_permissions, .. + }) => { + if let Some(server) = &self.server { + let member_roles = self + .member + .as_ref() + .map(|member| member.roles.clone()) + .unwrap_or_default(); + + let mut roles = role_permissions + .iter() + .filter(|(id, _)| member_roles.contains(id)) + .filter_map(|(id, permission)| { + server.roles.get(id).map(|role| { + let v: Override = (*permission).into(); + (role.rank, v) + }) + }) + .collect::>(); + + roles.sort_by(|a, b| b.0.cmp(&a.0)); + roles.into_iter().map(|(_, v)| v).collect() + } else { + vec![] + } + } + _ => vec![], + } + } else { + vec![] + } } /// Do we own this group or saved messages channel if it is one of those? async fn do_we_own_the_channel(&mut self) -> bool { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::Group { owner, .. }) + | Cow::Owned(Channel::Group { owner, .. }) => owner == &self.perspective.id, + Cow::Borrowed(Channel::SavedMessages { user, .. }) + | Cow::Owned(Channel::SavedMessages { user, .. }) => user == &self.perspective.id, + _ => false, + } + } else { + false + } } /// Are we a recipient of this channel? async fn are_we_part_of_the_channel(&mut self) -> bool { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::DirectMessage { recipients, .. }) + | Cow::Owned(Channel::DirectMessage { recipients, .. }) + | Cow::Borrowed(Channel::Group { recipients, .. }) + | Cow::Owned(Channel::Group { recipients, .. }) => { + recipients.contains(&self.perspective.id) + } + _ => false, + } + } else { + false + } } /// Set the current user as the recipient of this channel /// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths) async fn set_recipient_as_user(&mut self) { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::DirectMessage { recipients, .. }) + | Cow::Owned(Channel::DirectMessage { recipients, .. }) => { + let recipient_id = recipients + .iter() + .find(|recipient| recipient != &&self.perspective.id) + .expect("Missing recipient for DM"); + + if let Ok(user) = self.database.fetch_user(recipient_id).await { + self.user.replace(Cow::Owned(user)); + } + } + _ => unimplemented!(), + } + } } /// Set the current server as the server owning this channel /// (this will only ever be called for server channels, use unimplemented!() for other code paths) async fn set_server_from_channel(&mut self) { - todo!() + if let Some(channel) = &self.channel { + match channel { + Cow::Borrowed(Channel::TextChannel { server, .. }) + | Cow::Owned(Channel::TextChannel { server, .. }) + | Cow::Borrowed(Channel::VoiceChannel { server, .. }) + | Cow::Owned(Channel::VoiceChannel { server, .. }) => { + if let Ok(server) = self.database.fetch_server(server).await { + self.server.replace(Cow::Owned(server)); + } + } + _ => unimplemented!(), + } + } } } -impl<'a> PermissionCalculator<'a> { +impl<'a> DatabasePermissionQuery<'a> { /// Create a new permission calculator - pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { - PermissionCalculator { + pub fn new(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> { + DatabasePermissionQuery { database, perspective, user: None, + channel: None, + server: None, + member: None, cached_user_permission: None, cached_permission: None, @@ -167,7 +358,7 @@ impl<'a> PermissionCalculator<'a> { } /// Calculate the user permission value - pub async fn calc_user(mut self) -> PermissionCalculator<'a> { + pub async fn calc_user(mut self) -> DatabasePermissionQuery<'a> { if self.cached_user_permission.is_some() { return self; } @@ -176,14 +367,14 @@ impl<'a> PermissionCalculator<'a> { panic!("Expected `PermissionCalculator.user to exist."); } - PermissionCalculator { + DatabasePermissionQuery { cached_user_permission: Some(calculate_user_permissions(&mut self).await), ..self } } /// Calculate the permission value - pub async fn calc(self) -> PermissionCalculator<'a> { + pub async fn calc(self) -> DatabasePermissionQuery<'a> { if self.cached_permission.is_some() { return self; } @@ -192,15 +383,39 @@ impl<'a> PermissionCalculator<'a> { } /// Use user - pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator { - PermissionCalculator { - user: Some(user), + pub fn user(self, user: &'a User) -> DatabasePermissionQuery { + DatabasePermissionQuery { + user: Some(Cow::Borrowed(user)), + ..self + } + } + + /// Use channel + pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery { + DatabasePermissionQuery { + channel: Some(Cow::Borrowed(channel)), + ..self + } + } + + /// Use server + pub fn server(self, server: &'a Server) -> DatabasePermissionQuery { + DatabasePermissionQuery { + server: Some(Cow::Borrowed(server)), + ..self + } + } + + /// Use member + pub fn member(self, member: &'a Member) -> DatabasePermissionQuery { + DatabasePermissionQuery { + member: Some(Cow::Borrowed(member)), ..self } } } /// Short-hand for creating a permission calculator -pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { - PermissionCalculator::new(database, perspective) +pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> { + DatabasePermissionQuery::new(database, perspective) } diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index f45371fd..ea327d38 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -18,10 +18,15 @@ default = ["serde", "partials"] [dependencies] # Core +revolt-config = { version = "0.6.7", path = "../config" } revolt-permissions = { version = "0.6.7", path = "../permissions" } -# Serialisation +# Utility +regex = "1" indexmap = "1.9.3" +once_cell = "1.17.1" + +# Serialisation revolt_optional_struct = { version = "0.2.0", optional = true } serde = { version = "1", features = ["derive"], optional = true } iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] } diff --git a/crates/core/models/src/v0/bots.rs b/crates/core/models/src/v0/bots.rs index 8e71e715..3d1a1df3 100644 --- a/crates/core/models/src/v0/bots.rs +++ b/crates/core/models/src/v0/bots.rs @@ -1,7 +1,10 @@ use super::User; +use validator::Validate; + auto_derived!( /// Bot + #[derive(Default)] pub struct Bot { /// Bot Id #[cfg_attr(feature = "serde", serde(rename = "_id"))] @@ -55,6 +58,12 @@ auto_derived!( pub flags: u32, } + /// Optional fields on bot object + pub enum FieldsBot { + Token, + InteractionsURL, + } + /// Flags that may be attributed to a bot #[repr(u32)] pub enum BotFlags { @@ -71,10 +80,16 @@ auto_derived!( /// Bot Username pub username: String, /// Profile Avatar - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))] + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] pub avatar: String, /// Profile Description - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))] + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] pub description: String, } @@ -85,4 +100,68 @@ auto_derived!( /// User object pub user: User, } + + /// Bot Details + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataCreateBot { + /// Bot username + #[cfg_attr( + feature = "validator", + validate(length(min = 2, max = 32), regex = "super::RE_USERNAME") + )] + pub name: String, + } + + /// New Bot Details + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataEditBot { + /// Bot username + #[cfg_attr( + feature = "validator", + validate(length(min = 2, max = 32), regex = "super::RE_USERNAME") + )] + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// Whether the bot can be added by anyone + pub public: Option, + /// Whether analytics should be gathered for this bot + /// + /// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg). + pub analytics: Option, + /// Interactions URL + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))] + pub interactions_url: Option, + /// Fields to remove from bot object + #[cfg_attr(feature = "validator", validate(length(min = 1)))] + pub remove: Option>, + } + + /// Where we are inviting a bot to + #[serde(untagged)] + pub enum InviteBotDestination { + /// Invite to a server + Server { + /// Server Id + server: String, + }, + /// Invite to a group + Group { + /// Group Id + group: String, + }, + } + + /// Owned Bots Response + /// + /// Both lists are sorted by their IDs. + /// + /// TODO: user should be in bot object + pub struct OwnedBotsResponse { + /// Bot objects + pub bots: Vec, + /// User objects + pub users: Vec, + } ); diff --git a/crates/core/models/src/v0/messages.rs b/crates/core/models/src/v0/messages.rs index d67d44e6..438d8b26 100644 --- a/crates/core/models/src/v0/messages.rs +++ b/crates/core/models/src/v0/messages.rs @@ -1,7 +1,11 @@ +use std::time::SystemTime; + +use revolt_config::config; + use indexmap::{IndexMap, IndexSet}; use iso8601_timestamp::Timestamp; -use super::{Embed, File, MessageWebhook}; +use super::{Embed, File, MessageWebhook, User, Webhook}; auto_derived_partial!( /// Message @@ -129,11 +133,136 @@ auto_derived!( /// Sort by the oldest messages first Oldest, } + + /// Push Notification + pub struct PushNotification { + /// Known author name + pub author: String, + /// URL to author avatar + pub icon: String, + /// URL to first matching attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + /// Message content or system message information + pub body: String, + /// Unique tag, usually the channel ID + pub tag: String, + /// Timestamp at which this notification was created + pub timestamp: u64, + /// URL to open when clicking notification + pub url: String, + } ); +/// Message Author Abstraction +pub enum MessageAuthor<'a> { + User(&'a User), + Webhook(&'a Webhook), + System { + username: &'a str, + avatar: Option<&'a str>, + }, +} + impl Interactions { /// Check if default initialisation of fields pub fn is_default(&self) -> bool { !self.restrict_reactions && self.reactions.is_none() } } + +impl<'a> MessageAuthor<'a> { + pub fn id(&self) -> &str { + match self { + MessageAuthor::User(user) => &user.id, + MessageAuthor::Webhook(webhook) => &webhook.id, + MessageAuthor::System { .. } => "00000000000000000000000000", + } + } + + pub fn avatar(&self) -> Option<&str> { + match self { + MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()), + MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()), + MessageAuthor::System { avatar, .. } => *avatar, + } + } + + pub fn username(&self) -> &str { + match self { + MessageAuthor::User(user) => &user.username, + MessageAuthor::Webhook(webhook) => &webhook.name, + MessageAuthor::System { username, .. } => username, + } + } +} + +impl From for String { + fn from(s: SystemMessage) -> String { + match s { + SystemMessage::Text { content } => content, + SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(), + SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(), + SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(), + SystemMessage::UserLeft { .. } => "User left the channel.".to_string(), + SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(), + SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(), + SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(), + SystemMessage::ChannelDescriptionChanged { .. } => { + "Channel description changed.".to_string() + } + SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(), + SystemMessage::ChannelOwnershipChanged { .. } => { + "Channel ownership changed.".to_string() + } + } + } +} + +impl PushNotification { + /// Create a new notification from a given message, author and channel ID + pub async fn from(msg: Message, author: Option>, channel_id: &str) -> Self { + let config = config().await; + + let icon = if let Some(author) = &author { + if let Some(avatar) = author.avatar() { + format!("{}/avatars/{}", config.hosts.autumn, avatar) + } else { + format!("{}/users/{}/default_avatar", config.hosts.api, author.id()) + } + } else { + format!("{}/assets/logo.png", config.hosts.app) + }; + + let image = msg.attachments.and_then(|attachments| { + attachments + .first() + .map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id)) + }); + + let body = if let Some(sys) = msg.system { + sys.into() + } else if let Some(text) = msg.content { + text + } else { + "Empty Message".to_string() + }; + + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + Self { + author: author + .map(|x| x.username().to_string()) + .unwrap_or_else(|| "Revolt".to_string()), + icon, + image, + body, + tag: channel_id.to_string(), + timestamp, + url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id), + } + } +} diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index bb91ad26..3fbacce9 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -10,8 +10,7 @@ auto_derived_partial!( pub id: MemberCompositeKey, /// Time at which this user joined the server - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub joined_at: Option, + pub joined_at: Timestamp, /// Member's nickname #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] diff --git a/crates/core/models/src/v0/users.rs b/crates/core/models/src/v0/users.rs index 82fd9a91..07959ccf 100644 --- a/crates/core/models/src/v0/users.rs +++ b/crates/core/models/src/v0/users.rs @@ -1,6 +1,15 @@ +use once_cell::sync::Lazy; +use regex::Regex; + use super::File; -auto_derived!( +/// Regex for valid usernames +/// +/// Block zero width space +/// Block lookalike characters +pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); + +auto_derived_partial!( /// User pub struct User { /// Unique Id @@ -56,6 +65,18 @@ auto_derived!( pub relationship: RelationshipStatus, /// Whether this user is currently online pub online: bool, + }, + "PartialUser" +); + +auto_derived!( + /// Optional fields on user object + pub enum FieldsUser { + Avatar, + StatusText, + StatusPresence, + ProfileContent, + ProfileBackground, } /// User's relationship with another user (or themselves) diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index bf31e74c..a41b47d8 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -20,6 +20,9 @@ try-from-primitive = ["dep:num_enum"] async-std = { version = "1.8.0", features = ["attributes"] } [dependencies] +# Core +revolt-result = { version = "0.6.5", path = "../result" } + # Utility auto_ops = "0.3.0" once_cell = "1.17" diff --git a/crates/core/permissions/src/impl.rs b/crates/core/permissions/src/impl.rs index 11c378c6..2d424cf6 100644 --- a/crates/core/permissions/src/impl.rs +++ b/crates/core/permissions/src/impl.rs @@ -118,6 +118,10 @@ pub async fn calculate_channel_permissions(query: &mut P) -> permissions.restrict(*ALLOW_IN_TIMEOUT); } + if !permissions.has_channel_permission(ChannelPermission::ViewChannel) { + permissions.revoke_all(); + } + permissions } else { 0_u64.into() diff --git a/crates/core/permissions/src/models/channel.rs b/crates/core/permissions/src/models/channel.rs index d0aa2ecc..17762b1e 100644 --- a/crates/core/permissions/src/models/channel.rs +++ b/crates/core/permissions/src/models/channel.rs @@ -1,5 +1,5 @@ use once_cell::sync::Lazy; -use std::ops::Add; +use std::{fmt, ops::Add}; /// Abstract channel type pub enum ChannelType { @@ -102,6 +102,12 @@ pub enum ChannelPermission { GrantAll = u64::MAX, } +impl fmt::Display for ChannelPermission { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 }); impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 }); @@ -136,4 +142,9 @@ pub static DEFAULT_PERMISSION_SERVER: Lazy = Lazy::new(|| { ) }); -pub static DEFAULT_WEBHOOK_PERMISSIONS: Lazy = Lazy::new(|| ChannelPermission::SendMessage + ChannelPermission::SendEmbeds + ChannelPermission::Masquerade + ChannelPermission::React); +pub static DEFAULT_WEBHOOK_PERMISSIONS: Lazy = Lazy::new(|| { + ChannelPermission::SendMessage + + ChannelPermission::SendEmbeds + + ChannelPermission::Masquerade + + ChannelPermission::React +}); diff --git a/crates/core/permissions/src/models/mod.rs b/crates/core/permissions/src/models/mod.rs index e234eac5..657b9927 100644 --- a/crates/core/permissions/src/models/mod.rs +++ b/crates/core/permissions/src/models/mod.rs @@ -3,6 +3,7 @@ mod server; mod user; pub use channel::*; +use revolt_result::{create_error, Result}; pub use server::*; pub use user::*; @@ -27,10 +28,39 @@ impl PermissionValue { self.0 &= !v; } + /// Revoke all permissions + pub fn revoke_all(&mut self) { + self.0 = 0; + } + /// Restrict to given permissions pub fn restrict(&mut self, v: u64) { self.0 &= v; } + + /// Check whether certain a permission has been granted + pub fn has(&mut self, v: u64) -> bool { + (self.0 & v) == v + } + + /// Check whether certain a channel permission has been granted + pub fn has_channel_permission(&mut self, permission: ChannelPermission) -> bool { + self.has(permission as u64) + } + + /// Throw if missing channel permission + pub fn throw_if_lacking_channel_permission( + &mut self, + permission: ChannelPermission, + ) -> Result<()> { + if self.has_channel_permission(permission) { + Ok(()) + } else { + Err(create_error!(MissingPermission { + permission: permission.to_string() + })) + } + } } impl From for PermissionValue { diff --git a/crates/core/permissions/src/models/server.rs b/crates/core/permissions/src/models/server.rs index 6320248d..e4a47549 100644 --- a/crates/core/permissions/src/models/server.rs +++ b/crates/core/permissions/src/models/server.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; /// Representation of a single permission override -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "schemas", derive(JsonSchema))] pub struct Override { diff --git a/crates/core/permissions/src/models/user.rs b/crates/core/permissions/src/models/user.rs index 80a8da8f..1aa4c873 100644 --- a/crates/core/permissions/src/models/user.rs +++ b/crates/core/permissions/src/models/user.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// User's relationship with another user (or themselves) pub enum RelationshipStatus { None, @@ -21,5 +23,11 @@ pub enum UserPermission { Invite = 1 << 3, } +impl fmt::Display for UserPermission { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 }); impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 }); diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 7115405d..140a7a09 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -43,6 +43,7 @@ pub enum ErrorType { // ? User related errors UsernameTaken, InvalidUsername, + DiscriminatorChangeRatelimited, UnknownUser, AlreadyFriends, AlreadySentRequest, @@ -87,6 +88,7 @@ pub enum ErrorType { TooManyRoles { max: usize, }, + AlreadyInServer, // ? Bot related errors ReachedMaximumBots, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index a1723905..b6efd8cb 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -19,6 +19,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::UnknownUser => Status::NotFound, ErrorType::InvalidUsername => Status::BadRequest, ErrorType::UsernameTaken => Status::Conflict, + ErrorType::DiscriminatorChangeRatelimited => Status::TooManyRequests, ErrorType::AlreadyFriends => Status::Conflict, ErrorType::AlreadySentRequest => Status::Conflict, ErrorType::Blocked => Status::Conflict, @@ -42,6 +43,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::UnknownServer => Status::NotFound, ErrorType::InvalidRole => Status::NotFound, ErrorType::Banned => Status::Forbidden, + ErrorType::AlreadyInServer => Status::Conflict, ErrorType::TooManyServers { .. } => Status::BadRequest, ErrorType::TooManyEmoji { .. } => Status::BadRequest, diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index d4e663f6..d2773653 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -8,6 +8,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# Test +rand = "0.8.5" +redis-kiss = "0.1.4" + # Utility lru = "0.7.0" url = "2.2.2" @@ -51,6 +55,7 @@ lettre = "0.10.0-alpha.4" rocket = { version = "0.5.0-rc.2", default-features = false, features = [ "json", ] } +rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" } rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_authifier = { version = "1.0.7" } rocket_prometheus = "0.10.0-rc.3" diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 35654a04..f517f29b 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -8,8 +8,11 @@ extern crate serde_json; pub mod routes; pub mod util; +use rocket::{Build, Rocket}; +use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_prometheus::PrometheusMetrics; use std::net::Ipv4Addr; +use std::str::FromStr; use async_std::channel::unbounded; use revolt_quark::authifier::{Authifier, AuthifierEvent}; @@ -17,14 +20,7 @@ use revolt_quark::events::client::EventV1; use revolt_quark::DatabaseInfo; use rocket::data::ToByteUnit; -#[launch] -async fn rocket() -> _ { - // Configure logging and environment - revolt_quark::configure!(); - - // Ensure environment variables are present - revolt_quark::variables::delta::preflight_checks(); - +pub async fn web() -> Rocket { // Setup database let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); db.migrate_database().await.unwrap(); @@ -37,7 +33,7 @@ async fn rocket() -> _ { // Setup Authifier let authifier = Authifier { - database: legacy_db.clone().into(), + database: db.clone().into(), config: revolt_quark::util::authifier::config(), event_channel: Some(sender), }; @@ -59,10 +55,31 @@ async fn rocket() -> _ { }); // Launch background task workers + async_std::task::spawn(revolt_database::tasks::start_workers(db.clone())); async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone())); // Configure CORS - let cors = revolt_quark::web::cors::new(); + let cors = CorsOptions { + allowed_origins: AllowedOrigins::All, + allowed_methods: [ + "Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch", + ] + .iter() + .map(|s| FromStr::from_str(s).unwrap()) + .collect(), + ..Default::default() + } + .to_cors() + .expect("Failed to create CORS."); + + // Configure Swagger + let swagger = revolt_rocket_okapi::swagger_ui::make_swagger_ui( + &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { + url: "../openapi.json".to_owned(), + ..Default::default() + }, + ) + .into(); // Configure Rocket let rocket = rocket::build(); @@ -71,14 +88,14 @@ async fn rocket() -> _ { routes::mount(rocket) .attach(prometheus.clone()) .mount("/metrics", prometheus) - .mount("/", revolt_quark::web::cors::catch_all_options_routes()) - .mount("/", revolt_quark::web::ratelimiter::routes()) - .mount("/swagger/", revolt_quark::web::swagger::routes()) + .mount("/", rocket_cors::catch_all_options_routes()) + .mount("/", util::ratelimiter::routes()) + .mount("/swagger/", swagger) .manage(authifier) .manage(db) .manage(legacy_db) .manage(cors.clone()) - .attach(revolt_quark::web::ratelimiter::RatelimitFairing) + .attach(util::ratelimiter::RatelimitFairing) .attach(cors) .configure(rocket::Config { limits: rocket::data::Limits::default().limit("string", 5.megabytes()), @@ -86,3 +103,15 @@ async fn rocket() -> _ { ..Default::default() }) } + +#[launch] +async fn rocket() -> _ { + // Configure logging and environment + revolt_quark::configure!(); + + // Ensure environment variables are present + revolt_quark::variables::delta::preflight_checks(); + + // Start web server + web().await +} diff --git a/crates/delta/src/routes/bots/create.rs b/crates/delta/src/routes/bots/create.rs index 06f70089..f92b98e9 100644 --- a/crates/delta/src/routes/bots/create.rs +++ b/crates/delta/src/routes/bots/create.rs @@ -1,63 +1,59 @@ -use crate::util::regex::RE_USERNAME; - -use nanoid::nanoid; -use revolt_quark::{ - models::{user::BotInformation, Bot, User}, - variables::delta::MAX_BOT_COUNT, - Db, Error, Result, -}; - +use revolt_database::{Bot, Database, User}; +use revolt_models::v0; +use revolt_result::{create_error, Result}; use rocket::serde::json::Json; -use serde::Deserialize; -use ulid::Ulid; +use rocket::State; use validator::Validate; -/// # Bot Details -#[derive(Validate, Deserialize, JsonSchema)] -pub struct DataCreateBot { - /// Bot username - #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] - name: String, -} - /// # Create Bot /// /// Create a new Revolt bot. #[openapi(tag = "Bots")] #[post("/create", data = "")] -pub async fn create_bot(db: &Db, user: User, info: Json) -> Result> { - if user.bot.is_some() { - return Err(Error::IsBot); - } - +pub async fn create_bot( + db: &State, + user: User, + info: Json, +) -> Result> { let info = info.into_inner(); - info.validate() - .map_err(|error| Error::FailedValidation { error })?; + info.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - if db.get_number_of_bots_by_user(&user.id).await? >= *MAX_BOT_COUNT { - return Err(Error::ReachedMaximumBots); - } + let bot = Bot::create(db, info.name, &user, None).await?; + Ok(Json(bot.into())) +} + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn create_bot() { + let harness = TestHarness::new().await; + let (_, session, _) = harness.new_user().await; - let id = Ulid::new().to_string(); - let username = User::validate_username(info.name)?; - let bot_user = User { - id: id.clone(), - discriminator: User::find_discriminator(db, &username, None).await?, - username, - bot: Some(BotInformation { - owner: user.id.clone(), - }), - ..Default::default() - }; + let response = harness + .client + .post("/bots/create") + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataCreateBot { + name: TestHarness::rand_string(), + }) + .to_string(), + ) + .dispatch() + .await; - let bot = Bot { - id, - owner: user.id, - token: nanoid!(64), - ..Default::default() - }; + assert_eq!(response.status(), Status::Ok); - db.insert_user(&bot_user).await?; - db.insert_bot(&bot).await?; - Ok(Json(bot)) + let bot: v0::Bot = response.into_json().await.expect("`Bot`"); + assert!(harness.db.fetch_bot(&bot.id).await.is_ok()); + } } diff --git a/crates/delta/src/routes/bots/delete.rs b/crates/delta/src/routes/bots/delete.rs index 445deaa6..b9f4701a 100644 --- a/crates/delta/src/routes/bots/delete.rs +++ b/crates/delta/src/routes/bots/delete.rs @@ -1,19 +1,64 @@ -use revolt_quark::{models::User, Db, EmptyResponse, Error, Ref, Result}; +use revolt_database::{util::reference::Reference, Database, User}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; /// # Delete Bot /// /// Delete a bot by its id. #[openapi(tag = "Bots")] #[delete("/")] -pub async fn delete_bot(db: &Db, user: User, target: Ref) -> Result { - if user.bot.is_some() { - return Err(Error::IsBot); - } - +pub async fn delete_bot( + db: &State, + user: User, + target: Reference, +) -> Result { let bot = target.as_bot(db).await?; if bot.owner != user.id { - return Err(Error::NotFound); + return Err(create_error!(NotFound)); } bot.delete(db).await.map(|_| EmptyResponse) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Bot}; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn delete_bot() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .delete(format!("/bots/{}", bot.id)) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + assert!(harness.db.fetch_bot(&bot.id).await.is_err()); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::UserUpdate { id, .. } => id == &bot.id, + _ => false, + }) + .await; + + match event { + EventV1::UserUpdate { data, .. } => { + assert_eq!(data.flags, Some(2)); + } + _ => unreachable!(), + } + } +} diff --git a/crates/delta/src/routes/bots/edit.rs b/crates/delta/src/routes/bots/edit.rs index 7e22adb2..70170177 100644 --- a/crates/delta/src/routes/bots/edit.rs +++ b/crates/delta/src/routes/bots/edit.rs @@ -1,60 +1,32 @@ -use crate::util::regex::RE_USERNAME; - -use revolt_quark::{ - models::{ - bot::{FieldsBot, PartialBot}, - Bot, User, - }, - Db, Error, Ref, Result, -}; +use revolt_database::{util::reference::Reference, Database, PartialBot, User}; +use revolt_models::v0::{self, DataEditBot}; +use revolt_result::{create_error, Result}; +use rocket::State; use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; use validator::Validate; -/// # Bot Details -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct DataEditBot { - /// Bot username - #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - /// Whether the bot can be added by anyone - public: Option, - /// Whether analytics should be gathered for this bot - /// - /// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg). - analytics: Option, - /// Interactions URL - #[validate(length(min = 1, max = 2048))] - interactions_url: Option, - /// Fields to remove from bot object - #[validate(length(min = 1))] - remove: Option>, -} - /// # Edit Bot /// /// Edit bot details by its id. #[openapi(tag = "Bots")] #[patch("/", data = "")] pub async fn edit_bot( - db: &Db, + db: &State, user: User, - target: Ref, + target: Reference, data: Json, -) -> Result> { - if user.bot.is_some() { - return Err(Error::IsBot); - } - +) -> Result> { let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; let mut bot = target.as_bot(db).await?; if bot.owner != user.id { - return Err(Error::NotFound); + return Err(create_error!(NotFound)); } if let Some(name) = data.name { @@ -67,7 +39,7 @@ pub async fn edit_bot( && data.interactions_url.is_none() && data.remove.is_none() { - return Ok(Json(bot)); + return Ok(Json(bot.into())); } let DataEditBot { @@ -78,26 +50,63 @@ pub async fn edit_bot( .. } = data; - let mut partial = PartialBot { + let partial = PartialBot { public, analytics, interactions_url, ..Default::default() }; - if let Some(remove) = &remove { - for field in remove { - bot.remove(field); - } + bot.update( + db, + partial, + remove + .unwrap_or_default() + .into_iter() + .map(|v| v.into()) + .collect(), + ) + .await?; - if remove.iter().any(|x| x == &FieldsBot::Token) { - partial.token = Some(bot.token.clone()); - } - } + Ok(Json(bot.into())) +} + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::Bot; + use revolt_models::v0::{self, FieldsBot}; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn edit_bot() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .patch(format!("/bots/{}", bot.id)) + .header(ContentType::JSON) + .body( + json!(v0::DataEditBot { + public: Some(true), + remove: Some(vec![FieldsBot::Token]), + ..Default::default() + }) + .to_string(), + ) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; - db.update_bot(&bot.id, &partial, remove.unwrap_or_default()) - .await?; + assert_eq!(response.status(), Status::Ok); - bot.apply_options(partial); - Ok(Json(bot)) + let updated_bot: v0::Bot = response.into_json().await.expect("`Bot`"); + assert!(!bot.public); + assert!(updated_bot.public); + } } diff --git a/crates/delta/src/routes/bots/fetch.rs b/crates/delta/src/routes/bots/fetch.rs index 48aaaf5b..932895b4 100644 --- a/crates/delta/src/routes/bots/fetch.rs +++ b/crates/delta/src/routes/bots/fetch.rs @@ -1,6 +1,6 @@ -use revolt_database::{util::reference::Reference, Database}; +use revolt_database::{util::reference::Reference, Database, User}; use revolt_models::v0::FetchBotResponse; -use revolt_quark::{models::User, Error, Result}; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; /// # Fetch Bot @@ -14,21 +14,46 @@ pub async fn fetch_bot( bot: Reference, ) -> Result> { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } - let bot = bot.as_bot(db).await.map_err(Error::from_core)?; + let bot = bot.as_bot(db).await?; if bot.owner != user.id { - return Err(Error::NotFound); + return Err(create_error!(NotFound)); } Ok(Json(FetchBotResponse { - user: db - .fetch_user(&bot.id) - .await - .map_err(Error::from_core)? - .into(None) - .await, + user: db.fetch_user(&bot.id).await?.into(None).await, bot: bot.into(), })) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::Bot; + use revolt_models::v0; + use rocket::http::{Header, Status}; + + #[rocket::async_test] + async fn fetch_bot() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + let response = harness + .client + .get(format!("/bots/{}", bot.id)) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let response: v0::FetchBotResponse = response.into_json().await.expect("`Bot`"); + assert_eq!(response.bot, bot.into()); + } +} diff --git a/crates/delta/src/routes/bots/fetch_owned.rs b/crates/delta/src/routes/bots/fetch_owned.rs index f437fb7a..8a40b228 100644 --- a/crates/delta/src/routes/bots/fetch_owned.rs +++ b/crates/delta/src/routes/bots/fetch_owned.rs @@ -1,31 +1,16 @@ -use revolt_quark::{ - models::{Bot, User}, - Db, Error, Result, -}; +use futures::future::join_all; +use revolt_database::{Database, User}; +use revolt_models::v0::OwnedBotsResponse; +use revolt_result::Result; use rocket::serde::json::Json; -use serde::Serialize; - -/// # Owned Bots Response -/// -/// Both lists are sorted by their IDs. -#[derive(Serialize, JsonSchema)] -pub struct OwnedBotsResponse { - /// Bot objects - bots: Vec, - /// User objects - users: Vec, -} +use rocket::State; /// # Fetch Owned Bots /// /// Fetch all of the bots that you have control over. #[openapi(tag = "Bots")] #[get("/@me")] -pub async fn fetch_owned_bots(db: &Db, user: User) -> Result> { - if user.bot.is_some() { - return Err(Error::IsBot); - } - +pub async fn fetch_owned_bots(db: &State, user: User) -> Result> { let mut bots = db.fetch_bots_by_user(&user.id).await?; let user_ids = bots .iter() @@ -38,5 +23,41 @@ pub async fn fetch_owned_bots(db: &Db, user: User) -> Result`"); + assert_eq!(resp.bots.len(), 1); + assert_eq!(resp.users.len(), 1); + assert_eq!(resp.bots[0], bot.into()); + assert_eq!(resp.bots[0].id, resp.users[0].id); + } } diff --git a/crates/delta/src/routes/bots/fetch_public.rs b/crates/delta/src/routes/bots/fetch_public.rs index 258a9cd3..93ea1e7a 100644 --- a/crates/delta/src/routes/bots/fetch_public.rs +++ b/crates/delta/src/routes/bots/fetch_public.rs @@ -1,6 +1,6 @@ -use revolt_database::Database; +use revolt_database::{util::reference::Reference, Database, User}; use revolt_models::v0::PublicBot; -use revolt_quark::{models::User, Error, Ref, Result}; +use revolt_result::{create_error, Result}; use rocket::serde::json::Json; use rocket::State; @@ -13,13 +13,51 @@ use rocket::State; pub async fn fetch_public_bot( db: &State, user: Option, - target: Ref, + target: Reference, ) -> Result> { - let bot = db.fetch_bot(&target.id).await.map_err(Error::from_core)?; + let bot = db.fetch_bot(&target.id).await?; if !bot.public && user.map_or(true, |x| x.id != bot.owner) { - return Err(Error::NotFound); + return Err(create_error!(NotFound)); } - let user = db.fetch_user(&bot.id).await.map_err(Error::from_core)?; + let user = db.fetch_user(&bot.id).await?; Ok(Json(bot.into_public_bot(user))) } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{Bot, PartialBot}; + use revolt_models::v0; + + #[rocket::async_test] + async fn fetch_public() { + let harness = TestHarness::new().await; + let (_, _, user) = harness.new_user().await; + + let mut bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + bot.update( + &harness.db, + PartialBot { + public: Some(true), + ..Default::default() + }, + vec![], + ) + .await + .unwrap(); + + let bot_user = harness.db.fetch_user(&bot.id).await.expect("`User`"); + let response = harness + .client + .get(format!("/bots/{}/invite", bot.id)) + .dispatch() + .await; + + let public_bot: v0::PublicBot = response.into_json().await.expect("`PublicBot`"); + assert_eq!(public_bot, bot.into_public_bot(bot_user)); + } +} diff --git a/crates/delta/src/routes/bots/invite.rs b/crates/delta/src/routes/bots/invite.rs index 8cd61051..9677881d 100644 --- a/crates/delta/src/routes/bots/invite.rs +++ b/crates/delta/src/routes/bots/invite.rs @@ -1,23 +1,15 @@ -use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result}; +use revolt_database::util::permissions::DatabasePermissionQuery; +use revolt_database::Member; +use revolt_database::{util::reference::Reference, Database, User}; +use revolt_models::v0; +use revolt_permissions::{ + calculate_channel_permissions, calculate_server_permissions, ChannelPermission, +}; +use revolt_result::{create_error, Result}; +use rocket::State; use rocket::serde::json::Json; -use serde::Deserialize; - -/// # Invite Destination -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub enum InviteBotDestination { - /// Invite to a server - Server { - /// Server Id - server: String, - }, - /// Invite to a group - Group { - /// Group Id - group: String, - }, -} +use rocket_empty::EmptyResponse; /// # Invite Bot /// @@ -25,47 +17,168 @@ pub enum InviteBotDestination { #[openapi(tag = "Bots")] #[post("//invite", data = "")] pub async fn invite_bot( - db: &Db, + db: &State, user: User, - target: Ref, - dest: Json, + target: Reference, + dest: Json, ) -> Result { if user.bot.is_some() { - return Err(Error::IsBot); + return Err(create_error!(IsBot)); } let bot = target.as_bot(db).await?; if !bot.public && bot.owner != user.id { - return Err(Error::BotIsPrivate); + return Err(create_error!(BotIsPrivate)); } + let bot_user = db.fetch_user(&bot.id).await?; + match dest.into_inner() { - InviteBotDestination::Server { server } => { + v0::InviteBotDestination::Server { server } => { let server = db.fetch_server(&server).await?; - perms(&user) - .server(&server) - .throw_permission(db, Permission::ManageServer) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; - let user = db.fetch_user(&bot.id).await?; - server - .create_member(db, user, None) + Member::create(db, &server, &bot_user) .await .map(|_| EmptyResponse) } - InviteBotDestination::Group { group } => { + v0::InviteBotDestination::Group { group } => { let mut channel = db.fetch_channel(&group).await?; - perms(&user) - .channel(&channel) - .throw_permission_and_view_channel(db, Permission::InviteOthers) - .await?; + let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); + calculate_channel_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?; channel - .add_user_to_group(db, &bot.id, &user.id) + .add_user_to_group(db, &bot_user, &user.id) .await .map(|_| EmptyResponse) } } } + +#[cfg(test)] +mod test { + use crate::{rocket, util::test::TestHarness}; + use revolt_database::{events::client::EventV1, Bot, Channel, Server}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + + #[rocket::async_test] + async fn invite_bot_to_group() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + // FIXME: Channel::create_group + let group = Channel::Group { + id: ulid::Ulid::new().to_string(), + name: TestHarness::rand_string(), + owner: user.id.to_string(), + description: None, + last_message_id: None, + icon: None, + nsfw: false, + permissions: None, + recipients: vec![user.id.to_string()], + }; + + group.create(&harness.db).await.unwrap(); + + let response = harness + .client + .post(format!("/bots/{}/invite", bot.id)) + .header(ContentType::JSON) + .body(json!(v0::InviteBotDestination::Group { group: group.id() }).to_string()) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::ChannelGroupJoin { id, .. } => id == &group.id(), + _ => false, + }) + .await; + + match event { + EventV1::ChannelGroupJoin { user, .. } => { + assert_eq!(bot.id, user); + } + _ => unreachable!(), + } + } + + #[rocket::async_test] + async fn invite_bot_to_server() { + let mut harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) + .await + .expect("`Bot`"); + + // FIXME: Server::create_server + let server = Server { + id: ulid::Ulid::new().to_string(), + name: TestHarness::rand_string(), + owner: user.id.to_string(), + analytics: false, + discoverable: false, + nsfw: false, + banner: None, + icon: None, + categories: None, + channels: vec![], + default_permissions: 0, + description: None, + flags: None, + roles: Default::default(), + system_messages: None, + }; + + server.create(&harness.db).await.unwrap(); + + let response = harness + .client + .post(format!("/bots/{}/invite", bot.id)) + .header(ContentType::JSON) + .body( + json!(v0::InviteBotDestination::Server { + server: server.id.to_string() + }) + .to_string(), + ) + .header(Header::new("x-session-token", session.token.to_string())) + .dispatch() + .await; + + assert_eq!(response.status(), Status::NoContent); + drop(response); + + let event = harness + .wait_for_event(|event| match event { + EventV1::ServerMemberJoin { id, .. } => id == &server.id, + _ => false, + }) + .await; + + match event { + EventV1::ServerMemberJoin { user, .. } => { + assert_eq!(bot.id, user); + } + _ => unreachable!(), + } + } +} diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 455327e7..04034599 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -1,8 +1,8 @@ +use revolt_database::util::idempotency::IdempotencyKey; use revolt_quark::{ models::{message::DataMessageSend, Message, User}, perms, types::push::MessageAuthor, - web::idempotency::IdempotencyKey, Db, Error, Permission, Ref, Result, }; diff --git a/crates/delta/src/routes/customisation/emoji_create.rs b/crates/delta/src/routes/customisation/emoji_create.rs index 25ba7bf5..dcf28164 100644 --- a/crates/delta/src/routes/customisation/emoji_create.rs +++ b/crates/delta/src/routes/customisation/emoji_create.rs @@ -1,3 +1,5 @@ +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::models::emoji::EmojiParent; use revolt_quark::models::{Emoji, File, User}; use revolt_quark::variables::delta::MAX_EMOJI_COUNT; @@ -5,10 +7,13 @@ use revolt_quark::{perms, Db, Error, Permission, Result}; use serde::Deserialize; use validator::Validate; -use crate::util::regex::RE_EMOJI; - use rocket::serde::json::Json; +/// Regex for valid emoji names +/// +/// Alphanumeric and underscores +pub static RE_EMOJI: Lazy = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap()); + /// # Emoji Data #[derive(Validate, Deserialize, JsonSchema)] pub struct DataCreateEmoji { @@ -57,7 +62,9 @@ pub async fn create_emoji( // ! FIXME: hardcoded upper limit let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; if emojis.len() > *MAX_EMOJI_COUNT { - return Err(Error::TooManyEmoji { max: *MAX_EMOJI_COUNT }); + return Err(Error::TooManyEmoji { + max: *MAX_EMOJI_COUNT, + }); } } EmojiParent::Detached => return Err(Error::InvalidOperation), diff --git a/crates/delta/src/routes/mod.rs b/crates/delta/src/routes/mod.rs index 7b3f3e4c..b9b0fac0 100644 --- a/crates/delta/src/routes/mod.rs +++ b/crates/delta/src/routes/mod.rs @@ -25,7 +25,7 @@ pub fn mount(mut rocket: Rocket) -> Rocket { mount_endpoints_and_merged_docs! { rocket, "/".to_owned(), settings, "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root, root::ping], + "" => openapi_get_routes_spec![root::root], "/admin" => admin::routes(), "/users" => users::routes(), "/bots" => bots::routes(), @@ -46,7 +46,7 @@ pub fn mount(mut rocket: Rocket) -> Rocket { mount_endpoints_and_merged_docs! { rocket, "/".to_owned(), settings, "/" => (vec![], custom_openapi_spec()), - "" => openapi_get_routes_spec![root::root, root::ping], + "" => openapi_get_routes_spec![root::root], "/admin" => admin::routes(), "/users" => users::routes(), "/bots" => bots::routes(), diff --git a/crates/delta/src/routes/onboard/complete.rs b/crates/delta/src/routes/onboard/complete.rs index 90f7136f..51b43d7c 100644 --- a/crates/delta/src/routes/onboard/complete.rs +++ b/crates/delta/src/routes/onboard/complete.rs @@ -1,12 +1,20 @@ -use crate::util::regex::RE_USERNAME; -use revolt_quark::{ - authifier::models::Session, models::User, Database, EmptyResponse, Error, Result, -}; +use once_cell::sync::Lazy; +use regex::Regex; +use revolt_database::{Database, User}; +use revolt_models::v0; +use revolt_quark::authifier::models::Session; +use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; +/// Regex for valid usernames +/// +/// Block zero width space +/// Block lookalike characters +pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); + /// # New User Data #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataOnboard { @@ -25,22 +33,22 @@ pub async fn req( session: Session, user: Option, data: Json, -) -> Result { +) -> Result> { if user.is_some() { - return Err(Error::AlreadyOnboarded); + return Err(create_error!(AlreadyOnboarded)); } let data = data.into_inner(); - data.validate() - .map_err(|error| Error::FailedValidation { error })?; + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; - let username = User::validate_username(data.username)?; - let user = User { - id: session.user_id, - discriminator: User::find_discriminator(db, &username, None).await?, - username, - ..Default::default() - }; - - db.insert_user(&user).await.map(|_| EmptyResponse) + Ok(Json( + User::create(db, data.username, session.user_id, None) + .await? + .into_self() + .await, + )) } diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index c6743142..e223ad8d 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -4,7 +4,6 @@ use revolt_quark::variables::delta::{ }; use revolt_quark::Result; -use rocket::http::Status; use rocket::serde::json::Json; use serde::Serialize; @@ -138,9 +137,22 @@ pub async fn root() -> Result> { })) } -/// Example endpoint. -#[openapi(skip)] -#[get("/ping")] -pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status { - Status::Ok +#[cfg(test)] +mod test { + use crate::rocket; + use rocket::http::Status; + + #[rocket::async_test] + async fn hello_world() { + let harness = crate::util::test::TestHarness::new().await; + let response = harness.client.get("/").dispatch().await; + assert_eq!(response.status(), Status::Ok); + } + + #[rocket::async_test] + async fn hello_world_concurrent() { + let harness = crate::util::test::TestHarness::new().await; + let response = harness.client.get("/").dispatch().await; + assert_eq!(response.status(), Status::Ok); + } } diff --git a/crates/delta/src/routes/users/change_username.rs b/crates/delta/src/routes/users/change_username.rs index 2941a2eb..8d469f4c 100644 --- a/crates/delta/src/routes/users/change_username.rs +++ b/crates/delta/src/routes/users/change_username.rs @@ -1,9 +1,16 @@ -use crate::util::regex::RE_USERNAME; +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::{authifier::models::Account, models::User, Database, Error, Result}; use rocket::{serde::json::Json, State}; use serde::{Deserialize, Serialize}; use validator::Validate; +/// Regex for valid usernames +/// +/// Block zero width space +/// Block lookalike characters +pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); + /// # Username Information #[derive(Validate, Serialize, Deserialize, JsonSchema)] pub struct DataChangeUsername { diff --git a/crates/delta/src/routes/users/edit_user.rs b/crates/delta/src/routes/users/edit_user.rs index c1c2ba90..644a727d 100644 --- a/crates/delta/src/routes/users/edit_user.rs +++ b/crates/delta/src/routes/users/edit_user.rs @@ -1,3 +1,5 @@ +use once_cell::sync::Lazy; +use regex::Regex; use revolt_quark::models::user::{FieldsUser, PartialUser, User}; use revolt_quark::models::File; use revolt_quark::{Database, Error, Ref, Result}; @@ -8,7 +10,11 @@ use rocket::State; use serde::{Deserialize, Serialize}; use validator::Validate; -use crate::util::regex::RE_DISPLAY_NAME; +/// Regex for valid display names +/// +/// Block zero width space +/// Block newline and carriage return +pub static RE_DISPLAY_NAME: Lazy = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap()); /// # Profile Data #[derive(Validate, Serialize, Deserialize, Debug, JsonSchema)] diff --git a/crates/delta/src/routes/webhooks/webhook_execute.rs b/crates/delta/src/routes/webhooks/webhook_execute.rs index 0ec30299..fb31fa56 100644 --- a/crates/delta/src/routes/webhooks/webhook_execute.rs +++ b/crates/delta/src/routes/webhooks/webhook_execute.rs @@ -1,8 +1,10 @@ -use revolt_database::{util::reference::Reference, Database}; +use revolt_database::{ + util::{idempotency::IdempotencyKey, reference::Reference}, + Database, +}; use revolt_quark::{ models::message::{DataMessageSend, Message}, types::push::MessageAuthor, - web::idempotency::IdempotencyKey, Db, Error, Result, }; use rocket::{serde::json::Json, State}; diff --git a/crates/delta/src/util/mod.rs b/crates/delta/src/util/mod.rs index 1d9dab60..6ee3a7f6 100644 --- a/crates/delta/src/util/mod.rs +++ b/crates/delta/src/util/mod.rs @@ -1 +1,2 @@ -pub mod regex; +pub mod ratelimiter; +pub mod test; diff --git a/crates/quark/src/web/ratelimiter.rs b/crates/delta/src/util/ratelimiter.rs similarity index 97% rename from crates/quark/src/web/ratelimiter.rs rename to crates/delta/src/util/ratelimiter.rs index 58f581a3..814cf3e9 100644 --- a/crates/quark/src/web/ratelimiter.rs +++ b/crates/delta/src/util/ratelimiter.rs @@ -1,14 +1,9 @@ -//! Pulled from lightspeed-tv/backend. -//! -//! This will be replaced again in the near future since -//! I don't want duplication between two different projects. - use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; use std::ops::Add; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use crate::authifier::models::Session; +use revolt_quark::authifier::models::Session; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::Origin; use rocket::http::{Method, Status}; diff --git a/crates/delta/src/util/regex.rs b/crates/delta/src/util/regex.rs deleted file mode 100644 index 7c71e4b1..00000000 --- a/crates/delta/src/util/regex.rs +++ /dev/null @@ -1,19 +0,0 @@ -use once_cell::sync::Lazy; -use regex::Regex; - -/// Regex for valid display names -/// -/// Block zero width space -/// Block newline and carriage return -pub static RE_DISPLAY_NAME: Lazy = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap()); - -/// Regex for valid usernames -/// -/// Block zero width space -/// Block lookalike characters -pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); - -/// Regex for valid emoji names -/// -/// Alphanumeric and underscores -pub static RE_EMOJI: Lazy = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap()); diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs new file mode 100644 index 00000000..cd15ae22 --- /dev/null +++ b/crates/delta/src/util/test.rs @@ -0,0 +1,110 @@ +use futures::StreamExt; +use rand::Rng; +use redis_kiss::redis::aio::PubSub; +use revolt_database::{events::client::EventV1, Database, DatabaseInfo, User}; +use revolt_quark::authifier::{ + models::{Account, Session}, + Authifier, +}; +use rocket::local::asynchronous::Client; + +pub struct TestHarness { + pub client: Client, + authifier: Authifier, + pub db: Database, + sub: PubSub, + event_buffer: Vec, +} + +impl TestHarness { + pub async fn new() -> TestHarness { + dotenv::dotenv().ok(); + + let client = Client::tracked(crate::web().await) + .await + .expect("valid rocket instance"); + + let db = DatabaseInfo::Auto.connect().await.expect("`Database`"); + let mut sub = redis_kiss::open_pubsub_connection() + .await + .expect("`PubSub`"); + + sub.psubscribe("*").await.unwrap(); + + TestHarness { + client, + authifier: Authifier { + database: db.clone().into(), + config: revolt_quark::util::authifier::config(), + event_channel: None, + }, + db, + sub, + event_buffer: vec![], + } + } + + pub fn rand_string() -> String { + let mut rng = rand::thread_rng(); + (&mut rng) + .sample_iter(rand::distributions::Alphanumeric) + .take(20) + .map(char::from) + .collect() + } + + pub async fn new_user(&self) -> (Account, Session, User) { + let account = Account::new( + &self.authifier, + format!("{}@revolt.chat", TestHarness::rand_string()), + "password".to_string(), + false, + ) + .await + .expect("`Account`"); + + let session = account + .create_session(&self.authifier, String::new()) + .await + .expect("`Session`"); + + let user = User::create( + &self.db, + TestHarness::rand_string(), + account.id.to_string(), + None, + ) + .await + .expect("`User`"); + + (account, session, user) + } + + pub async fn wait_for_event(&mut self, predicate: F) -> EventV1 + where + F: Fn(&EventV1) -> bool, + { + for event in &self.event_buffer { + if predicate(event) { + // does not remove from buffer + return event.clone(); + } + } + + let mut stream = self.sub.on_message(); + while let Some(item) = stream.next().await { + let payload: EventV1 = redis_kiss::decode_payload(&item.unwrap()).unwrap(); + + if predicate(&payload) { + return payload; + } + + self.event_buffer.push(payload); + } + + // WARNING: if predicate is never satisfied, this will never return + // should add a timeout for events so tests can fail gracefully + + unreachable!() + } +} diff --git a/crates/quark/src/impl/dummy/mod.rs b/crates/quark/src/impl/dummy/mod.rs index 194f2edb..cd72470d 100644 --- a/crates/quark/src/impl/dummy/mod.rs +++ b/crates/quark/src/impl/dummy/mod.rs @@ -23,7 +23,6 @@ pub mod servers { } pub mod users { - pub mod bot; pub mod user; pub mod user_settings; } diff --git a/crates/quark/src/impl/dummy/users/bot.rs b/crates/quark/src/impl/dummy/users/bot.rs deleted file mode 100644 index e2e36c7e..00000000 --- a/crates/quark/src/impl/dummy/users/bot.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::models::bot::{Bot, FieldsBot, PartialBot}; -use crate::{AbstractBot, Result}; - -use super::super::DummyDb; - -#[async_trait] -impl AbstractBot for DummyDb { - async fn fetch_bot(&self, id: &str) -> Result { - Ok(Bot { - id: id.into(), - owner: "user".into(), - token: "token".into(), - public: true, - analytics: true, - discoverable: true, - ..Default::default() - }) - } - - async fn fetch_bot_by_token(&self, _token: &str) -> Result { - self.fetch_bot("bot").await - } - - async fn insert_bot(&self, bot: &Bot) -> Result<()> { - info!("Insert {bot:?}"); - Ok(()) - } - - async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec) -> Result<()> { - info!("Update {id} with {bot:?} and remove {remove:?}"); - Ok(()) - } - - async fn delete_bot(&self, id: &str) -> Result<()> { - info!("Delete {id}"); - Ok(()) - } - - async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { - Ok(vec![self.fetch_bot(user_id).await.unwrap()]) - } - - async fn get_number_of_bots_by_user(&self, _user_id: &str) -> Result { - Ok(1) - } -} diff --git a/crates/quark/src/impl/generic/channels/channel.rs b/crates/quark/src/impl/generic/channels/channel.rs index b9b135e8..d020585f 100644 --- a/crates/quark/src/impl/generic/channels/channel.rs +++ b/crates/quark/src/impl/generic/channels/channel.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use revolt_database::util::idempotency::IdempotencyKey; use ulid::Ulid; use crate::{ @@ -11,8 +12,7 @@ use crate::{ }, tasks::{ack::AckEvent, process_embeds}, types::push::MessageAuthor, - variables::delta::{MAX_ATTACHMENT_COUNT, MAX_REPLY_COUNT, MAX_EMBED_COUNT}, - web::idempotency::IdempotencyKey, + variables::delta::{MAX_ATTACHMENT_COUNT, MAX_EMBED_COUNT, MAX_REPLY_COUNT}, Database, Error, OverrideField, Ref, Result, }; @@ -413,7 +413,10 @@ impl Channel { ) -> Result { Message::validate_sum(&data.content, data.embeds.as_deref().unwrap_or_default())?; - idempotency.consume_nonce(data.nonce).await?; + idempotency + .consume_nonce(data.nonce) + .await + .map_err(|_| Error::InvalidOperation)?; // Check the message is not empty if (data.content.as_ref().map_or(true, |v| v.is_empty())) @@ -497,16 +500,24 @@ impl Channel { // Add attachments to message. let mut attachments = vec![]; - if data.attachments.as_ref().is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT) { + if data + .attachments + .as_ref() + .is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT) + { return Err(Error::TooManyAttachments { max: *MAX_ATTACHMENT_COUNT, }); } - if data.embeds.as_ref().is_some_and(|v| v.len() > *MAX_EMBED_COUNT) { + if data + .embeds + .as_ref() + .is_some_and(|v| v.len() > *MAX_EMBED_COUNT) + { return Err(Error::TooManyEmbeds { - max: *MAX_EMBED_COUNT - }) + max: *MAX_EMBED_COUNT, + }); } for attachment_id in data.attachments.as_deref().unwrap_or_default() { diff --git a/crates/quark/src/impl/generic/mod.rs b/crates/quark/src/impl/generic/mod.rs index b050cae5..cd4d13b3 100644 --- a/crates/quark/src/impl/generic/mod.rs +++ b/crates/quark/src/impl/generic/mod.rs @@ -19,7 +19,6 @@ pub mod servers { } pub mod users { - pub mod bot; pub mod user; pub mod user_settings; } diff --git a/crates/quark/src/impl/generic/users/bot.rs b/crates/quark/src/impl/generic/users/bot.rs deleted file mode 100644 index 2a327735..00000000 --- a/crates/quark/src/impl/generic/users/bot.rs +++ /dev/null @@ -1,24 +0,0 @@ -use nanoid::nanoid; - -use crate::{ - models::{bot::FieldsBot, Bot}, - Database, Result, -}; - -impl Bot { - /// Remove a field from this object - pub fn remove(&mut self, field: &FieldsBot) { - match field { - FieldsBot::Token => self.token = nanoid!(64), - FieldsBot::InteractionsURL => { - self.interactions_url.take(); - } - } - } - - /// Delete this bot - pub async fn delete(&self, db: &Database) -> Result<()> { - db.fetch_user(&self.id).await?.mark_deleted(db).await?; - db.delete_bot(&self.id).await - } -} diff --git a/crates/quark/src/impl/generic/users/user.rs b/crates/quark/src/impl/generic/users/user.rs index 38609b0f..2eaa894a 100644 --- a/crates/quark/src/impl/generic/users/user.rs +++ b/crates/quark/src/impl/generic/users/user.rs @@ -243,6 +243,8 @@ impl User { return Err(Error::DiscriminatorChangeRatelimited); } + // FIXME: don't access directly? + #[allow(clippy::disallowed_methods)] rvdb.insert_ratelimit_event(&revolt_database::RatelimitEvent { id: ulid::Ulid::new().to_string(), target_id, @@ -469,7 +471,17 @@ impl User { #[async_recursion] pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result { match hint { - UserHint::Bot => db.fetch_user(&db.fetch_bot_by_token(token).await?.id).await, + UserHint::Bot => { + let rvdb: revolt_database::Database = db.clone().into(); + db.fetch_user( + &rvdb + .fetch_bot_by_token(token) + .await + .map_err(|_| Error::InternalError)? + .id, + ) + .await + } UserHint::User => db.fetch_user_by_token(token).await, UserHint::Any => { if let Ok(user) = User::from_token(db, token, UserHint::User).await { diff --git a/crates/quark/src/impl/mongo/mod.rs b/crates/quark/src/impl/mongo/mod.rs index 7711ab0f..b04cd9f2 100644 --- a/crates/quark/src/impl/mongo/mod.rs +++ b/crates/quark/src/impl/mongo/mod.rs @@ -34,7 +34,6 @@ pub mod servers { } pub mod users { - pub mod bot; pub mod user; pub mod user_settings; } diff --git a/crates/quark/src/impl/mongo/users/bot.rs b/crates/quark/src/impl/mongo/users/bot.rs deleted file mode 100644 index 7dc5ff85..00000000 --- a/crates/quark/src/impl/mongo/users/bot.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::models::bot::{Bot, FieldsBot, PartialBot}; -use crate::r#impl::mongo::IntoDocumentPath; -use crate::{AbstractBot, Result}; - -use super::super::MongoDb; - -static COL: &str = "bots"; - -#[async_trait] -impl AbstractBot for MongoDb { - async fn fetch_bot(&self, id: &str) -> Result { - self.find_one_by_id(COL, id).await - } - - async fn fetch_bot_by_token(&self, token: &str) -> Result { - self.find_one( - COL, - doc! { - "token": token - }, - ) - .await - } - - async fn insert_bot(&self, bot: &Bot) -> Result<()> { - self.insert_one(COL, &bot).await.map(|_| ()) - } - - async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec) -> Result<()> { - self.update_one_by_id( - COL, - id, - bot, - remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), - None, - ) - .await - .map(|_| ()) - } - - async fn delete_bot(&self, id: &str) -> Result<()> { - self.delete_one_by_id(COL, id).await.map(|_| ()) - } - - async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { - self.find( - COL, - doc! { - "owner": user_id - }, - ) - .await - } - - async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result { - // ! FIXME: move this to generic? - self.fetch_bots_by_user(user_id).await.map(|x| x.len()) - } -} - -impl IntoDocumentPath for FieldsBot { - fn as_path(&self) -> Option<&'static str> { - match self { - FieldsBot::InteractionsURL => Some("interactions_url"), - FieldsBot::Token => None, - } - } -} diff --git a/crates/quark/src/models/mod.rs b/crates/quark/src/models/mod.rs index 370c72a1..4bd26af1 100644 --- a/crates/quark/src/models/mod.rs +++ b/crates/quark/src/models/mod.rs @@ -22,7 +22,6 @@ mod servers { } mod users { - pub mod bot; pub mod user; pub mod user_settings; } @@ -40,7 +39,6 @@ pub use servers::*; pub use users::*; pub use attachment::File; -pub use bot::Bot; pub use channel::Channel; pub use channel_invite::Invite; pub use channel_unread::ChannelUnread; diff --git a/crates/quark/src/traits/channels/channel.rs b/crates/quark/src/traits/channels/channel.rs index e51c5ce0..d4436574 100644 --- a/crates/quark/src/traits/channels/channel.rs +++ b/crates/quark/src/traits/channels/channel.rs @@ -1,4 +1,4 @@ -use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}}; +use crate::models::channel::{Channel, FieldsChannel, PartialChannel}; use crate::{OverrideField, Result}; #[async_trait] @@ -13,9 +13,6 @@ pub trait AbstractChannel: Sync + Send { async fn insert_channel(&self, channel: &Channel) -> Result<()>; /// Update an existing channel using some data - /// ! TODO: we need separate Channel::update which also sends out the relevant events - /// ! also applies to other methods I guess, try to restrict event bound methods to - /// ! the models themselves instead of the abstract database async fn update_channel( &self, id: &str, diff --git a/crates/quark/src/traits/mod.rs b/crates/quark/src/traits/mod.rs index 341565a1..bda97615 100644 --- a/crates/quark/src/traits/mod.rs +++ b/crates/quark/src/traits/mod.rs @@ -21,7 +21,6 @@ mod servers { } mod users { - pub mod bot; pub mod user; pub mod user_settings; } @@ -45,7 +44,6 @@ pub use servers::server::AbstractServer; pub use servers::server_ban::AbstractServerBan; pub use servers::server_member::AbstractServerMember; -pub use users::bot::AbstractBot; pub use users::user::AbstractUser; pub use users::user_settings::AbstractUserSettings; @@ -65,7 +63,6 @@ pub trait AbstractDatabase: + AbstractServer + AbstractServerBan + AbstractServerMember - + AbstractBot + AbstractUser + AbstractUserSettings + AbstractReport diff --git a/crates/quark/src/traits/users/bot.rs b/crates/quark/src/traits/users/bot.rs deleted file mode 100644 index e9421580..00000000 --- a/crates/quark/src/traits/users/bot.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::models::bot::{Bot, FieldsBot, PartialBot}; -use crate::Result; - -#[async_trait] -pub trait AbstractBot: Sync + Send { - /// Fetch a bot by its id - async fn fetch_bot(&self, id: &str) -> Result; - - /// Fetch a bot by its token - async fn fetch_bot_by_token(&self, token: &str) -> Result; - - /// Insert new bot into the database - async fn insert_bot(&self, bot: &Bot) -> Result<()>; - - /// Update bot with new information - async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec) -> Result<()>; - - /// Delete a bot from the database - async fn delete_bot(&self, id: &str) -> Result<()>; - - /// Fetch bots owned by a user - async fn fetch_bots_by_user(&self, user_id: &str) -> Result>; - - /// Get the number of bots owned by a user - async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result; -} diff --git a/crates/quark/src/util/ref.rs b/crates/quark/src/util/ref.rs index 1b3ff148..110ad501 100644 --- a/crates/quark/src/util/ref.rs +++ b/crates/quark/src/util/ref.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::models::{ - Bot, Channel, Emoji, Invite, Member, Message, Report, Server, ServerBan, User, + Channel, Emoji, Invite, Member, Message, Report, Server, ServerBan, User, }; use crate::{Database, Error, Result}; @@ -56,11 +56,6 @@ impl Ref { Ok(message) } - /// Fetch bot from Ref - pub async fn as_bot(&self, db: &Database) -> Result { - db.fetch_bot(&self.id).await - } - /// Fetch invite from Ref pub async fn as_invite(&self, db: &Database) -> Result { Invite::find(db, &self.id).await diff --git a/crates/quark/src/web/cors.rs b/crates/quark/src/web/cors.rs deleted file mode 100644 index 6ce5ec0d..00000000 --- a/crates/quark/src/web/cors.rs +++ /dev/null @@ -1,19 +0,0 @@ -use std::str::FromStr; - -pub use rocket_cors::catch_all_options_routes; -use rocket_cors::{AllowedOrigins, Cors}; - -pub fn new() -> Cors { - rocket_cors::CorsOptions { - allowed_origins: AllowedOrigins::All, - allowed_methods: [ - "Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch", - ] - .iter() - .map(|s| FromStr::from_str(s).unwrap()) - .collect(), - ..Default::default() - } - .to_cors() - .expect("Failed to create CORS.") -} diff --git a/crates/quark/src/web/mod.rs b/crates/quark/src/web/mod.rs index cbe9a41c..b9a1cff7 100644 --- a/crates/quark/src/web/mod.rs +++ b/crates/quark/src/web/mod.rs @@ -1,10 +1,5 @@ use crate::Database; use rocket::State; -pub mod cors; -pub mod idempotency; -pub mod ratelimiter; -pub mod swagger; - pub use rocket_empty::EmptyResponse; pub type Db = State; diff --git a/crates/quark/src/web/swagger.rs b/crates/quark/src/web/swagger.rs deleted file mode 100644 index fcfe3a91..00000000 --- a/crates/quark/src/web/swagger.rs +++ /dev/null @@ -1,9 +0,0 @@ -use rocket::Route; - -pub fn routes() -> Vec { - revolt_rocket_okapi::swagger_ui::make_swagger_ui(&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "../openapi.json".to_owned(), - ..Default::default() - }) - .into() -} diff --git a/default.nix b/default.nix new file mode 100644 index 00000000..a8ff2a27 --- /dev/null +++ b/default.nix @@ -0,0 +1,33 @@ +let + # Pinned nixpkgs, deterministic. Last updated: 11-08-2023. + pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/bb9707ef2ea4a5b749b362d5cf81ada3ded2c53f.tar.gz")) {}; + + # Rolling updates, not deterministic. + # pkgs = import (fetchTarball("channel:nixpkgs-unstable")) {}; +in pkgs.mkShell { + name = "revoltEnv"; + + # LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + # pkgs.gcc-unwrapped + # pkgs.zlib + # pkgs.glib + # pkgs.libGL + # ]; + + buildInputs = [ + # Tools + pkgs.git + + # Database + # pkgs.mongodb + + # Rust + pkgs.cargo + pkgs.rustc + pkgs.clippy + pkgs.pkgconfig + pkgs.openssl.dev + ]; + + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; +}