diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index c7f57608..d41ff722 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -2,7 +2,6 @@ name: Rust build, test, and generate specification on: push: - branches: [master] pull_request: branches: [master] @@ -27,34 +26,40 @@ jobs: with: command: build + - name: Run services in background + run: | + docker-compose -f docker-compose.db.yml up -d + - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test + - name: Run cargo test (with MongoDB) + uses: actions-rs/cargo@v1 + env: + MONGODB: mongodb://localhost + with: + command: test + - name: Copy .env.example - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' run: | cp .env.example .env - - name: Run services in background - if: github.event_name != 'pull_request' - run: | - docker-compose -f docker-compose.db.yml up -d - - name: Start API in background - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' run: | cargo run --bin revolt-delta & - name: Wait for API to go up - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' uses: nev7n/wait_for_response@v1 with: url: "http://localhost:8000/" - name: Checkout API repository - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' uses: actions/checkout@v3 with: repository: revoltchat/api @@ -62,11 +67,11 @@ jobs: token: ${{ secrets.PAT }} - name: Download OpenAPI specification - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json - name: Commit changes - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref_name == 'master' uses: EndBug/add-and-commit@v4 with: cwd: "api" diff --git a/Cargo.lock b/Cargo.lock index 9aa64794..5129b4cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -199,20 +199,20 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", - "quote 1.0.18", - "syn 1.0.107", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] name = "async-std" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-attributes", "async-channel", @@ -229,7 +229,6 @@ dependencies = [ "kv-log-macro", "log", "memchr", - "num_cpus", "once_cell", "pin-project-lite 0.2.9", "pin-utils", @@ -269,7 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -286,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -715,7 +714,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" dependencies = [ - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -747,7 +746,7 @@ dependencies = [ "fnv", "ident_case", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "strsim", "syn 1.0.107", ] @@ -759,7 +758,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -808,7 +807,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -829,7 +828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" dependencies = [ "devise_core", - "quote 1.0.18", + "quote 1.0.26", ] [[package]] @@ -841,7 +840,7 @@ dependencies = [ "bitflags", "proc-macro2", "proc-macro2-diagnostics", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -909,7 +908,7 @@ checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" dependencies = [ "heck", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -929,7 +928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -1113,7 +1112,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -1204,7 +1203,7 @@ checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" dependencies = [ "proc-macro-error", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -1842,7 +1841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2110,7 +2109,7 @@ checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2163,7 +2162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2186,15 +2185,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "optional_struct" -version = "0.2.0" -source = "git+https://github.com/insertish/OptionalStruct?rev=ee56427cee1f007839825d93d07fffd5a5e038c7#ee56427cee1f007839825d93d07fffd5a5e038c7" -dependencies = [ - "quote 0.3.15", - "syn 0.11.11", -] - [[package]] name = "os_info" version = "3.4.0" @@ -2268,7 +2258,7 @@ checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2306,7 +2296,7 @@ dependencies = [ "pest", "pest_meta", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2337,7 +2327,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2424,7 +2414,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", "version_check", ] @@ -2436,15 +2426,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2456,7 +2446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", "version_check", "yansi", @@ -2482,9 +2472,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" [[package]] name = "quote" -version = "1.0.18" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -2722,7 +2712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -2817,12 +2807,31 @@ dependencies = [ "log", "once_cell", "querystring", + "revolt-presence", "revolt-quark", "rmp-serde", "serde", "serde_json", ] +[[package]] +name = "revolt-database" +version = "0.0.1" +dependencies = [ + "async-recursion", + "async-std", + "async-trait", + "authifier", + "futures", + "log", + "mongodb", + "nanoid", + "revolt-result", + "revolt_optional_struct", + "serde", + "serde_json", +] + [[package]] name = "revolt-delta" version = "0.5.19" @@ -2845,6 +2854,8 @@ dependencies = [ "once_cell", "regex", "reqwest", + "revolt-database", + "revolt-models", "revolt-quark", "revolt_rocket_okapi", "rocket", @@ -2859,6 +2870,26 @@ dependencies = [ "vergen", ] +[[package]] +name = "revolt-models" +version = "0.0.1" +dependencies = [ + "revolt-database", + "schemars", + "serde", +] + +[[package]] +name = "revolt-presence" +version = "0.0.1" +dependencies = [ + "async-std", + "log", + "once_cell", + "rand 0.8.5", + "redis-kiss", +] + [[package]] name = "revolt-quark" version = "0.5.19" @@ -2886,13 +2917,15 @@ dependencies = [ "nanoid", "num_enum", "once_cell", - "optional_struct", "pretty_env_logger", "rand 0.8.5", "redis-kiss", "regex", "reqwest", + "revolt-presence", + "revolt-result", "revolt_okapi", + "revolt_optional_struct", "revolt_rocket_okapi", "rocket", "rocket_cors", @@ -2907,6 +2940,14 @@ dependencies = [ "web-push", ] +[[package]] +name = "revolt-result" +version = "0.0.1" +dependencies = [ + "schemars", + "serde", +] + [[package]] name = "revolt_okapi" version = "0.9.1" @@ -2919,6 +2960,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "revolt_optional_struct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d342739658623fe9d72b42f7a36ba19094d1d9bf9b423c3e337ab203c15d53e" +dependencies = [ + "quote 0.3.15", + "syn 0.11.11", +] + [[package]] name = "revolt_rocket_okapi" version = "0.9.1" @@ -2943,7 +2994,7 @@ checksum = "cc6620569d8ac8f0a1690fcca13f488503807a60e96ebf729749b59aca1dbef9" dependencies = [ "darling", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "rocket_http", "syn 1.0.107", ] @@ -3050,7 +3101,7 @@ dependencies = [ "glob", "indexmap", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "rocket_http", "syn 1.0.107", "unicode-xid 0.2.3", @@ -3225,7 +3276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "serde_derive_internals", "syn 1.0.107", ] @@ -3378,9 +3429,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] @@ -3396,13 +3447,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", - "quote 1.0.18", - "syn 1.0.107", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] @@ -3412,15 +3463,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "indexmap", "itoa 1.0.2", @@ -3458,7 +3509,7 @@ checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ "darling", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -3633,7 +3684,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote 1.0.26", "unicode-ident", ] @@ -3705,7 +3767,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -3814,7 +3876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -3950,7 +4012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -4080,7 +4142,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", ] @@ -4290,7 +4352,7 @@ dependencies = [ "lazy_static", "proc-macro-error", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "regex", "syn 1.0.107", "validator_types", @@ -4400,7 +4462,7 @@ dependencies = [ "lazy_static", "log", "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", "wasm-bindgen-shared", ] @@ -4423,7 +4485,7 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ - "quote 1.0.18", + "quote 1.0.26", "wasm-bindgen-macro-support", ] @@ -4434,7 +4496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", - "quote 1.0.18", + "quote 1.0.26", "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", diff --git a/Cargo.toml b/Cargo.toml index b51fd959..a5a13117 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*"] +members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] [patch.crates-io] # mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..bb3ea02e --- /dev/null +++ b/clippy.toml @@ -0,0 +1,7 @@ +disallowed-methods = [ + # Shouldn't need to access these directly + "revolt_database::models::bots::model::Bot::remove_field", + + # Prefer to use Object::delete() + "revolt_database::models::bots::ops::AbstractBots::update_bot", +] diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 14dca258..c7d4604d 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -26,3 +26,6 @@ serde = "1.0.136" futures = "0.3.21" async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] } async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] } + +# core +revolt-presence = { path = "../core/presence" } diff --git a/crates/bonfire/src/main.rs b/crates/bonfire/src/main.rs index 8fe3b2f3..e9bcef88 100644 --- a/crates/bonfire/src/main.rs +++ b/crates/bonfire/src/main.rs @@ -1,7 +1,7 @@ use std::env; use async_std::net::TcpListener; -use revolt_quark::presence::presence_clear_region; +use revolt_presence::clear_region; #[macro_use] extern crate log; @@ -18,7 +18,7 @@ async fn main() { database::connect().await; // Clean up the current region information. - presence_clear_region(None).await; + clear_region(None).await; // Setup a TCP listener to accept WebSocket connections on. // By default, we bind to port 9000 on all interfaces. diff --git a/crates/bonfire/src/websocket.rs b/crates/bonfire/src/websocket.rs index e298db3e..c2bbead6 100644 --- a/crates/bonfire/src/websocket.rs +++ b/crates/bonfire/src/websocket.rs @@ -1,6 +1,7 @@ use std::net::SocketAddr; use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt}; +use revolt_presence::{create_session, delete_session}; use revolt_quark::{ events::{ client::EventV1, @@ -8,7 +9,6 @@ use revolt_quark::{ state::{State, SubscriptionStateChange}, }, models::{user::UserHint, User}, - presence::{presence_create_session, presence_delete_session}, redis_kiss, Database, }; @@ -69,8 +69,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) let user_id = state.cache.user_id.clone(); // Create presence session. - let (first_session, session_id) = - presence_create_session(&user_id, 0).await; + let (first_session, session_id) = create_session(&user_id, 0).await; // Notify socket we have authenticated. write @@ -225,7 +224,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) } // Clean up presence session. - let last_session = presence_delete_session(&user_id, session_id).await; + let last_session = delete_session(&user_id, session_id).await; // If this was the last session, notify other users that we just went offline. if last_session { diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml new file mode 100644 index 00000000..336c01cf --- /dev/null +++ b/crates/core/database/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "revolt-database" +version = "0.0.1" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = [ "Paul Makles " ] +description = "Revolt Backend: Database Implementation" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +# Databases +mongodb = [ "dep:mongodb" ] + +# ... Other +async-std-runtime = [ "async-std" ] + +# Default Features +default = [ "mongodb", "async-std-runtime" ] + +[dependencies] +# Core +revolt-result = { version = "0.0.1", path = "../result" } + +# Utility +log = "0.4" +nanoid = "0.4.0" + +# Serialisation +serde_json = "1" +revolt_optional_struct = "0.2.0" +serde = { version = "1", features = ["derive"] } + +# Database +mongodb = { optional = true, version = "2.1.0", default-features = false } + +# Async Language Features +futures = "0.3.19" +async-trait = "0.1.51" +async-recursion = "1.0.4" + +# Async +async-std = { version = "1.8.0", features = ["attributes"], optional = true } + +# Authifier +authifier = { version = "1.0" } diff --git a/crates/core/database/src/drivers/mod.rs b/crates/core/database/src/drivers/mod.rs new file mode 100644 index 00000000..79cc03f1 --- /dev/null +++ b/crates/core/database/src/drivers/mod.rs @@ -0,0 +1,67 @@ +mod mongodb; +mod reference; + +pub use self::mongodb::*; +pub use self::reference::*; + +/// Database information to use to create a client +pub enum DatabaseInfo { + /// Auto-detect the database in use + Auto, + /// Auto-detect the database in use and create an empty testing database + Test(String), + /// Use the mock database + Reference, + /// Connect to MongoDB + MongoDb { uri: String, database_name: String }, + /// Use existing MongoDB connection + MongoDbFromClient(::mongodb::Client, String), +} + +/// Database +#[derive(Clone)] +pub enum Database { + /// Mock database + Reference(ReferenceDb), + /// MongoDB database + MongoDb(MongoDb), +} + +impl DatabaseInfo { + /// Create a database client from the given database information + #[async_recursion] + pub async fn connect(self) -> Result { + Ok(match self { + DatabaseInfo::Auto => { + if let Ok(uri) = std::env::var("MONGODB") { + return DatabaseInfo::MongoDb { + uri, + database_name: "revolt".to_string(), + } + .connect() + .await; + } + + DatabaseInfo::Reference.connect().await? + } + DatabaseInfo::Test(database_name) => { + if let Ok(uri) = std::env::var("MONGODB") { + return DatabaseInfo::MongoDb { uri, database_name }.connect().await; + } + + DatabaseInfo::Reference.connect().await? + } + DatabaseInfo::Reference => Database::Reference(Default::default()), + DatabaseInfo::MongoDb { uri, database_name } => { + let client = ::mongodb::Client::with_uri_str(uri) + .await + .map_err(|_| "Failed to init db connection.".to_string())?; + + Database::MongoDb(MongoDb(client, database_name)) + } + DatabaseInfo::MongoDbFromClient(client, database_name) => { + Database::MongoDb(MongoDb(client, database_name)) + } + }) + } +} diff --git a/crates/core/database/src/drivers/mongodb.rs b/crates/core/database/src/drivers/mongodb.rs new file mode 100644 index 00000000..74e5c6ef --- /dev/null +++ b/crates/core/database/src/drivers/mongodb.rs @@ -0,0 +1,243 @@ +use std::collections::HashMap; +use std::ops::Deref; + +use futures::StreamExt; +use mongodb::bson::{doc, to_document, Document}; +use mongodb::error::Result; +use mongodb::options::{FindOneOptions, FindOptions}; +use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +database_derived!( + #[cfg(feature = "mongodb")] + /// MongoDB implementation + pub struct MongoDb(pub ::mongodb::Client, pub String); +); + +impl Deref for MongoDb { + type Target = mongodb::Client; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[allow(dead_code)] +impl MongoDb { + /// Get the Revolt database + pub fn db(&self) -> mongodb::Database { + self.database(&self.1) + } + + /// Get a collection by its name + pub fn col(&self, collection: &str) -> mongodb::Collection { + self.db().collection(collection) + } + + /// Insert one document into a collection + pub async fn insert_one( + &self, + collection: &'static str, + document: T, + ) -> Result { + self.col::(collection).insert_one(document, None).await + } + + /// Count documents by projection + pub async fn count_documents( + &self, + collection: &'static str, + projection: Document, + ) -> Result { + self.col::(collection) + .count_documents(projection, None) + .await + } + + /// Find multiple documents in a collection with options + pub async fn find_with_options( + &self, + collection: &'static str, + projection: Document, + options: O, + ) -> Result> + where + O: Into>, + { + Ok(self + .col::(collection) + .find(projection, options) + .await? + .filter_map(|s| async { + if cfg!(debug_assertions) { + // Hard fail on invalid documents + Some(s.unwrap()) + } else { + s.ok() + } + }) + .collect::>() + .await) + } + + /// Find multiple documents in a collection + pub async fn find( + &self, + collection: &'static str, + projection: Document, + ) -> Result> { + self.find_with_options(collection, projection, None).await + } + + /// Find one document with options + pub async fn find_one_with_options( + &self, + collection: &'static str, + projection: Document, + options: O, + ) -> Result> + where + O: Into>, + { + self.col::(collection) + .find_one(projection, options) + .await + } + + /// Find one document + pub async fn find_one( + &self, + collection: &'static str, + projection: Document, + ) -> Result> { + self.find_one_with_options(collection, projection, None) + .await + } + + /// Find one document by its ID + pub async fn find_one_by_id( + &self, + collection: &'static str, + id: &str, + ) -> Result> { + self.find_one( + collection, + doc! { + "_id": id + }, + ) + .await + } + + /// Update one document given a projection, partial document, and list of paths to unset + pub async fn update_one( + &self, + collection: &'static str, + projection: Document, + partial: T, + remove: Vec<&dyn IntoDocumentPath>, + prefix: P, + ) -> Result + where + P: Into>, + { + let prefix = prefix.into(); + + let mut unset = doc! {}; + for field in remove { + if let Some(path) = field.as_path() { + if let Some(prefix) = &prefix { + unset.insert(prefix.to_owned() + path, 1_i32); + } else { + unset.insert(path, 1_i32); + } + } + } + + let query = doc! { + "$unset": unset, + "$set": if let Some(prefix) = &prefix { + to_document(&prefix_keys(&partial, prefix)) + } else { + to_document(&partial) + }? + }; + + self.col::(collection) + .update_one(projection, query, None) + .await + } + + /// Update one document given an ID, partial document, and list of paths to unset + pub async fn update_one_by_id( + &self, + collection: &'static str, + id: &str, + partial: T, + remove: Vec<&dyn IntoDocumentPath>, + prefix: P, + ) -> Result + where + P: Into>, + { + self.update_one( + collection, + doc! { + "_id": id + }, + partial, + remove, + prefix, + ) + .await + } + + /// Delete one document by the given projection + pub async fn delete_one( + &self, + collection: &'static str, + projection: Document, + ) -> Result { + self.col::(collection) + .delete_one(projection, None) + .await + } + + /// Delete one document by the given ID + pub async fn delete_one_by_id( + &self, + collection: &'static str, + id: &str, + ) -> Result { + self.delete_one( + collection, + doc! { + "_id": id + }, + ) + .await + } +} + +/// Just a string ID struct +#[derive(Deserialize)] +pub struct DocumentId { + #[serde(rename = "_id")] + pub id: String, +} + +pub trait IntoDocumentPath: Send + Sync { + /// Create JSON key path + fn as_path(&self) -> Option<&'static str>; +} + +/// Prefix keys on an arbitrary object +pub fn prefix_keys(t: &T, prefix: &str) -> HashMap { + let v: String = serde_json::to_string(t).unwrap(); + let v: HashMap = serde_json::from_str(&v).unwrap(); + v.into_iter() + .filter(|(_k, v)| !v.is_null()) + .map(|(k, v)| (prefix.to_owned() + &k, v)) + .collect() +} diff --git a/crates/core/database/src/drivers/reference.rs b/crates/core/database/src/drivers/reference.rs new file mode 100644 index 00000000..e14ff6f3 --- /dev/null +++ b/crates/core/database/src/drivers/reference.rs @@ -0,0 +1,13 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures::lock::Mutex; + +use crate::Bot; + +database_derived!( + /// Reference implementation + #[derive(Default)] + pub struct ReferenceDb { + pub bots: Arc>>, + } +); diff --git a/crates/core/database/src/lib.rs b/crates/core/database/src/lib.rs new file mode 100644 index 00000000..4796b684 --- /dev/null +++ b/crates/core/database/src/lib.rs @@ -0,0 +1,81 @@ +#[macro_use] +extern crate serde; + +#[macro_use] +extern crate async_recursion; + +#[macro_use] +extern crate async_trait; + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate revolt_optional_struct; + +#[macro_use] +extern crate revolt_result; + +#[cfg(feature = "mongodb")] +pub use mongodb; + +macro_rules! database_derived { + ( $( $item:item )+ ) => { + $( + #[derive(Clone)] + $item + )+ + }; +} + +macro_rules! auto_derived { + ( $( $item:item )+ ) => { + $( + #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] + $item + )+ + }; +} + +macro_rules! auto_derived_partial { + ( $item:item, $name:expr ) => { + #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] + #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] + #[optional_name = $name] + #[opt_skip_serializing_none] + #[opt_some_priority] + $item + }; +} + +mod drivers; +pub use drivers::*; + +#[cfg(test)] +macro_rules! database_test { + ( | $db: ident | $test:expr ) => { + let db = $crate::DatabaseInfo::Test(format!( + "{}:{}", + file!().replace('/', "_").replace(".rs", ""), + line!() + )) + .connect() + .await + .expect("Database connection failed."); + + db.drop_database().await; + + #[allow(clippy::redundant_closure_call)] + (|$db: $crate::Database| $test)(db.clone()).await; + + db.drop_database().await + }; +} + +mod models; +pub use models::*; + +/// Utility function to check if a boolean value is false +pub fn if_false(t: &bool) -> bool { + !t +} diff --git a/crates/core/database/src/models/admin_migrations/mod.rs b/crates/core/database/src/models/admin_migrations/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/admin_migrations/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/admin_migrations/model.rs b/crates/core/database/src/models/admin_migrations/model.rs new file mode 100644 index 00000000..70bbc62c --- /dev/null +++ b/crates/core/database/src/models/admin_migrations/model.rs @@ -0,0 +1,24 @@ +auto_derived!( + /// Document representing migration information + pub struct MigrationInfo { + /// Unique Id + #[serde(rename = "_id")] + pub id: i32, + /// Current database revision + pub revision: i32, + } +); + +#[cfg(test)] +mod tests { + #[async_std::test] + async fn migrate() { + database_test!(|db| async move { + // Initialise the database + db.migrate_database().await.unwrap(); + + // Migrate the existing database + db.migrate_database().await.unwrap() + }); + } +} diff --git a/crates/core/database/src/models/admin_migrations/ops.rs b/crates/core/database/src/models/admin_migrations/ops.rs new file mode 100644 index 00000000..ff8ae25d --- /dev/null +++ b/crates/core/database/src/models/admin_migrations/ops.rs @@ -0,0 +1,12 @@ +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractMigrations: Sync + Send { + #[cfg(test)] + /// Drop the database + async fn drop_database(&self); + + /// Migrate the database + async fn migrate_database(&self) -> Result<(), ()>; +} diff --git a/crates/quark/src/impl/mongo/admin/migrations.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb.rs similarity index 57% rename from crates/quark/src/impl/mongo/admin/migrations.rs rename to crates/core/database/src/models/admin_migrations/ops/mongodb.rs index 943ca78f..cdc684e9 100644 --- a/crates/quark/src/impl/mongo/admin/migrations.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb.rs @@ -1,13 +1,20 @@ -use crate::{AbstractMigrations, Result}; +use crate::MongoDb; -use super::super::MongoDb; +use super::AbstractMigrations; mod init; mod scripts; #[async_trait] impl AbstractMigrations for MongoDb { - async fn migrate_database(&self) -> Result<()> { + #[cfg(test)] + /// Drop the database + async fn drop_database(&self) { + self.db().drop(None).await.ok(); + } + + /// Migrate the database + async fn migrate_database(&self) -> Result<(), ()> { info!("Migrating the database."); let list = self @@ -15,7 +22,7 @@ impl AbstractMigrations for MongoDb { .await .expect("Failed to fetch database names."); - if list.iter().any(|x| x == "revolt") { + if list.iter().any(|x| x == &self.1) { scripts::migrate_database(self).await; } else { init::create_database(self).await; diff --git a/crates/quark/src/impl/mongo/admin/migrations/init.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs similarity index 97% rename from crates/quark/src/impl/mongo/admin/migrations/init.rs rename to crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs index 4501381c..2649635d 100644 --- a/crates/quark/src/impl/mongo/admin/migrations/init.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/init.rs @@ -1,9 +1,8 @@ -use crate::r#impl::mongo::MongoDb; - use super::scripts::LATEST_REVISION; -use mongodb::bson::doc; -use mongodb::options::CreateCollectionOptions; +use crate::mongodb::bson::doc; +use crate::mongodb::options::CreateCollectionOptions; +use crate::MongoDb; pub async fn create_database(db: &MongoDb) { info!("Creating database."); diff --git a/crates/quark/src/impl/mongo/admin/migrations/scripts.rs b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs similarity index 98% rename from crates/quark/src/impl/mongo/admin/migrations/scripts.rs rename to crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs index 168029e6..8c5aa874 100644 --- a/crates/quark/src/impl/mongo/admin/migrations/scripts.rs +++ b/crates/core/database/src/models/admin_migrations/ops/mongodb/scripts.rs @@ -1,15 +1,15 @@ -use std::{time::Duration, ops::BitXor}; +use std::{ops::BitXor, time::Duration}; -use bson::{Bson, DateTime}; -use futures::StreamExt; -use mongodb::{ - bson::{doc, from_bson, from_document, to_document, Document}, - options::FindOptions, +use crate::{ + mongodb::{ + bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document}, + options::FindOptions, + }, + MongoDb, }; +use futures::StreamExt; use serde::{Deserialize, Serialize}; -use crate::{r#impl::mongo::MongoDb, Permission, DEFAULT_PERMISSION_SERVER}; - #[derive(Serialize, Deserialize)] struct MigrationInfo { _id: i32, @@ -504,7 +504,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { update.insert( "default_permissions", // Remove Send Message permission if it wasn't originally granted - DEFAULT_PERMISSION_SERVER.bitxor(if has_send { 0 } else { Permission::SendMessage as u64}) as i64, + (4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64, ); if let Some(Bson::Document(mut roles)) = document.remove("roles") { @@ -563,7 +563,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 { doc! { "default_permissions": { "a": 0_i64, - "d": Permission::SendMessage as i64 + "d": (1 << 22) as i64 } }, ); diff --git a/crates/core/database/src/models/admin_migrations/ops/reference.rs b/crates/core/database/src/models/admin_migrations/ops/reference.rs new file mode 100644 index 00000000..a0913ae0 --- /dev/null +++ b/crates/core/database/src/models/admin_migrations/ops/reference.rs @@ -0,0 +1,16 @@ +use crate::ReferenceDb; + +use super::AbstractMigrations; + +#[async_trait] +impl AbstractMigrations for ReferenceDb { + #[cfg(test)] + /// Drop the database + async fn drop_database(&self) {} + + /// Migrate the database + async fn migrate_database(&self) -> Result<(), ()> { + // Here you would do your typical migrations if this was a real database. + Ok(()) + } +} diff --git a/crates/core/database/src/models/bots/mod.rs b/crates/core/database/src/models/bots/mod.rs new file mode 100644 index 00000000..4d801b73 --- /dev/null +++ b/crates/core/database/src/models/bots/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod ops; + +pub use model::*; +pub use ops::*; diff --git a/crates/core/database/src/models/bots/model.rs b/crates/core/database/src/models/bots/model.rs new file mode 100644 index 00000000..a8c551e0 --- /dev/null +++ b/crates/core/database/src/models/bots/model.rs @@ -0,0 +1,152 @@ +use revolt_result::Result; + +use crate::Database; + +auto_derived_partial!( + /// Bot + pub struct Bot { + /// Bot Id + /// + /// This equals the associated bot user's id. + #[serde(rename = "_id")] + pub id: String, + /// User Id of the bot owner + pub owner: String, + /// Token used to authenticate requests for this bot + pub token: String, + /// Whether the bot is public + /// (may be invited by anyone) + pub public: bool, + + /// Whether to enable analytics + #[serde(skip_serializing_if = "crate::if_false", default)] + pub analytics: bool, + /// Whether this bot should be publicly discoverable + #[serde(skip_serializing_if = "crate::if_false", default)] + pub discoverable: bool, + /// Reserved; URL for handling interactions + #[serde(skip_serializing_if = "String::is_empty", default)] + pub interactions_url: String, + /// URL for terms of service + #[serde(skip_serializing_if = "String::is_empty", default)] + pub terms_of_service_url: String, + /// URL for privacy policy + #[serde(skip_serializing_if = "String::is_empty", default)] + pub privacy_policy_url: String, + + /// Enum of bot flags + #[serde(skip_serializing_if = "Option::is_none")] + pub flags: Option, + }, + "PartialBot" +); + +auto_derived!( + /// Flags that may be attributed to a bot + #[repr(i32)] + pub enum BotFlags { + Verified = 1, + Official = 2, + } + + /// Optional fields on bot object + pub enum FieldsBot { + Token, + InteractionsURL, + } +); + +#[allow(clippy::disallowed_methods)] +impl Bot { + /// Remove a field from this object + pub fn remove_field(&mut self, field: &FieldsBot) { + match field { + FieldsBot::Token => self.token = nanoid::nanoid!(64), + FieldsBot::InteractionsURL => { + self.interactions_url = String::new(); + } + } + } + + /// Update this bot + pub async fn update( + &mut self, + db: &Database, + mut partial: PartialBot, + remove: Vec, + ) -> Result<()> { + if remove.contains(&FieldsBot::Token) { + partial.token = Some(nanoid::nanoid!(64)); + } + + for field in &remove { + self.remove_field(field); + } + + db.update_bot(&self.id, &partial, remove).await?; + + self.apply_options(partial); + Ok(()) + } + + /// 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 + } +} + +#[cfg(test)] +mod tests { + use crate::{Bot, FieldsBot, PartialBot}; + + #[async_std::test] + async fn crud() { + database_test!(|db| async move { + let bot_id = "bot"; + let user_id = "user"; + let token = "my_token"; + + 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(); + + let mut updated_bot = bot.clone(); + updated_bot + .update( + &db, + PartialBot { + public: Some(true), + ..Default::default() + }, + vec![FieldsBot::Token, FieldsBot::InteractionsURL], + ) + .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(); + + assert!(!bot.public); + assert!(fetched_bot1.public); + assert!(!bot.interactions_url.is_empty()); + assert!(fetched_bot1.interactions_url.is_empty()); + assert_ne!(bot.token, fetched_bot1.token); + 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()); + + 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()) + }); + } +} diff --git a/crates/core/database/src/models/bots/ops.rs b/crates/core/database/src/models/bots/ops.rs new file mode 100644 index 00000000..3a618253 --- /dev/null +++ b/crates/core/database/src/models/bots/ops.rs @@ -0,0 +1,35 @@ +use revolt_result::Result; + +use crate::{Bot, FieldsBot, PartialBot}; + +mod mongodb; +mod reference; + +#[async_trait] +pub trait AbstractBots: 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, + partial: &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/core/database/src/models/bots/ops/mongodb.rs b/crates/core/database/src/models/bots/ops/mongodb.rs new file mode 100644 index 00000000..bda3ae02 --- /dev/null +++ b/crates/core/database/src/models/bots/ops/mongodb.rs @@ -0,0 +1,93 @@ +use ::mongodb::bson::doc; +use revolt_result::Result; + +use crate::{Bot, FieldsBot, PartialBot}; +use crate::{IntoDocumentPath, MongoDb}; + +use super::AbstractBots; + +static COL: &str = "bots"; + +#[async_trait] +impl AbstractBots for MongoDb { + /// Fetch a bot by its id + async fn fetch_bot(&self, id: &str) -> Result { + query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a bot by its token + async fn fetch_bot_by_token(&self, token: &str) -> Result { + query!( + self, + find_one, + COL, + doc! { + "token": token + } + )? + .ok_or_else(|| create_error!(NotFound)) + } + + /// Insert new bot into the database + async fn insert_bot(&self, bot: &Bot) -> Result<()> { + query!(self, insert_one, COL, &bot).map(|_| ()) + } + + /// Update bot with new information + async fn update_bot( + &self, + id: &str, + partial: &PartialBot, + remove: Vec, + ) -> Result<()> { + query!( + self, + update_one_by_id, + COL, + id, + partial, + remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(), + None + ) + .map(|_| ()) + } + + /// Delete a bot from the database + async fn delete_bot(&self, id: &str) -> Result<()> { + query!(self, delete_one_by_id, COL, id).map(|_| ()) + } + + /// Fetch bots owned by a user + async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { + query!( + self, + find, + COL, + doc! { + "owner": user_id + } + ) + } + + /// Get the number of bots owned by a user + async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result { + query!( + self, + count_documents, + COL, + doc! { + "owner": user_id + } + ) + .map(|v| v as usize) + } +} + +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/core/database/src/models/bots/ops/reference.rs b/crates/core/database/src/models/bots/ops/reference.rs new file mode 100644 index 00000000..3a8caff2 --- /dev/null +++ b/crates/core/database/src/models/bots/ops/reference.rs @@ -0,0 +1,82 @@ +use revolt_result::Result; + +use crate::ReferenceDb; +use crate::{Bot, FieldsBot, PartialBot}; + +use super::AbstractBots; + +#[async_trait] +impl AbstractBots for ReferenceDb { + /// Fetch a bot by its id + async fn fetch_bot(&self, id: &str) -> Result { + let bots = self.bots.lock().await; + bots.get(id).cloned().ok_or_else(|| create_error!(NotFound)) + } + + /// Fetch a bot by its token + async fn fetch_bot_by_token(&self, token: &str) -> Result { + let bots = self.bots.lock().await; + bots.values() + .find(|bot| bot.token == token) + .cloned() + .ok_or_else(|| create_error!(NotFound)) + } + + /// Insert new bot into the database + async fn insert_bot(&self, bot: &Bot) -> Result<()> { + let mut bots = self.bots.lock().await; + if bots.contains_key(&bot.id) { + Err(create_database_error!("insert", "bot")) + } else { + bots.insert(bot.id.to_string(), bot.clone()); + Ok(()) + } + } + + /// Update bot with new information + async fn update_bot( + &self, + id: &str, + partial: &PartialBot, + remove: Vec, + ) -> Result<()> { + let mut bots = self.bots.lock().await; + if let Some(bot) = bots.get_mut(id) { + for field in remove { + #[allow(clippy::disallowed_methods)] + bot.remove_field(&field); + } + + bot.apply_options(partial.clone()); + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Delete a bot from the database + async fn delete_bot(&self, id: &str) -> Result<()> { + let mut bots = self.bots.lock().await; + if bots.remove(id).is_some() { + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + + /// Fetch bots owned by a user + async fn fetch_bots_by_user(&self, user_id: &str) -> Result> { + let bots = self.bots.lock().await; + Ok(bots + .values() + .filter(|bot| bot.owner == user_id) + .cloned() + .collect()) + } + + /// Get the number of bots owned by a user + async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result { + let bots = self.bots.lock().await; + Ok(bots.values().filter(|bot| bot.owner == user_id).count()) + } +} diff --git a/crates/core/database/src/models/mod.rs b/crates/core/database/src/models/mod.rs new file mode 100644 index 00000000..d6b2a21c --- /dev/null +++ b/crates/core/database/src/models/mod.rs @@ -0,0 +1,26 @@ +mod admin_migrations; +mod bots; + +pub use admin_migrations::*; +pub use bots::*; + +use crate::{Database, MongoDb, ReferenceDb}; + +pub trait AbstractDatabase: + Sync + Send + admin_migrations::AbstractMigrations + bots::AbstractBots +{ +} + +impl AbstractDatabase for ReferenceDb {} +impl AbstractDatabase for MongoDb {} + +impl std::ops::Deref for Database { + type Target = dyn AbstractDatabase; + + fn deref(&self) -> &Self::Target { + match &self { + Database::Reference(dummy) => dummy, + Database::MongoDb(mongo) => mongo, + } + } +} diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml new file mode 100644 index 00000000..152363ad --- /dev/null +++ b/crates/core/models/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "revolt-models" +version = "0.0.1" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = [ "Paul Makles " ] +description = "Revolt Backend: API Models" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +serde = [ "dep:serde" ] +schemas = [ "dep:schemars" ] +from_database = [ "revolt-database" ] + +default = [ "serde", "from_database" ] + +[dependencies] +# Repo +revolt-database = { version = "0.0.1", path = "../database", optional = true } + +# Serialisation +serde = { version = "1", features = ["derive"], optional = true } + +# Spec Generation +schemars = { version = "0.8.8", optional = true } diff --git a/crates/core/models/src/bots.rs b/crates/core/models/src/bots.rs new file mode 100644 index 00000000..7c201e68 --- /dev/null +++ b/crates/core/models/src/bots.rs @@ -0,0 +1,103 @@ +auto_derived!( + /// # Bot + pub struct Bot { + /// Bot Id + #[serde(rename = "_id")] + pub id: String, + + /// User Id of the bot owner + #[serde(rename = "owner")] + pub owner_id: String, + /// Token used to authenticate requests for this bot + pub token: String, + /// Whether the bot is public + /// (may be invited by anyone) + pub public: bool, + + /// Whether to enable analytics + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "crate::if_false", default) + )] + pub analytics: bool, + /// Whether this bot should be publicly discoverable + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "crate::if_false", default) + )] + pub discoverable: bool, + /// Reserved; URL for handling interactions + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] + pub interactions_url: String, + /// URL for terms of service + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] + pub terms_of_service_url: String, + /// URL for privacy policy + #[cfg_attr( + feature = "serde", + serde(skip_serializing_if = "String::is_empty", default) + )] + pub privacy_policy_url: String, + + /// Enum of bot flags + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub flags: Option, + } + + /// # Public Bot + pub struct PublicBot { + /// Bot Id + #[serde(rename = "_id")] + id: String, + + /// Bot Username + username: String, + /// Profile Avatar + #[serde(skip_serializing_if = "String::is_empty")] + avatar: String, + /// Profile Description + #[serde(skip_serializing_if = "String::is_empty")] + description: String, + } +); + +#[cfg(feature = "from_database")] +impl PublicBot { + pub fn from( + bot: revolt_database::Bot, + username: String, + avatar: Option, + description: Option, + ) -> Self { + PublicBot { + id: bot.id, + username, + avatar: avatar.unwrap_or_default(), + description: description.unwrap_or_default(), + } + } +} + +#[cfg(feature = "from_database")] +impl From for Bot { + fn from(value: revolt_database::Bot) -> Self { + Bot { + id: value.id, + owner_id: value.owner, + token: value.token, + public: value.public, + analytics: value.analytics, + discoverable: value.discoverable, + interactions_url: value.interactions_url, + terms_of_service_url: value.terms_of_service_url, + privacy_policy_url: value.privacy_policy_url, + flags: value.flags, + } + } +} diff --git a/crates/core/models/src/lib.rs b/crates/core/models/src/lib.rs new file mode 100644 index 00000000..21f199c8 --- /dev/null +++ b/crates/core/models/src/lib.rs @@ -0,0 +1,27 @@ +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde; + +#[cfg(feature = "schemas")] +#[macro_use] +extern crate schemars; + +macro_rules! auto_derived { + ( $( $item:item )+ ) => { + $( + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[cfg_attr(feature = "schemas", derive(JsonSchema))] + #[derive(Debug, Clone, Eq, PartialEq)] + $item + )+ + }; +} + +mod bots; + +pub use bots::*; + +/// Utility function to check if a boolean value is false +pub fn if_false(t: &bool) -> bool { + !t +} diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml new file mode 100644 index 00000000..7f904775 --- /dev/null +++ b/crates/core/presence/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "revolt-presence" +version = "0.0.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dev-dependencies] +# Async +async-std = { version = "1.8.0", features = ["attributes"] } + +[dependencies] +# Utility +log = "0.4.17" +rand = "0.8.5" +once_cell = "1.17.1" + +# Redis +redis-kiss = "0.1.4" diff --git a/crates/quark/src/presence/mod.rs b/crates/core/presence/src/lib.rs similarity index 60% rename from crates/quark/src/presence/mod.rs rename to crates/core/presence/src/lib.rs index fe50537b..72418734 100644 --- a/crates/quark/src/presence/mod.rs +++ b/crates/core/presence/src/lib.rs @@ -1,20 +1,31 @@ +#[macro_use] +extern crate log; + +use once_cell::sync::Lazy; +use rand::Rng; +use redis_kiss::{get_connection, AsyncCommands}; use std::collections::HashSet; -use redis_kiss::{get_connection, AsyncCommands}; - -use rand::Rng; -mod entry; mod operations; - use operations::{ __add_to_set_string, __add_to_set_u32, __delete_key, __get_set_members_as_string, __get_set_size, __remove_from_set_string, __remove_from_set_u32, }; -use self::entry::{ONLINE_SET, REGION_KEY}; +pub static REGION_ID: Lazy = Lazy::new(|| { + std::env::var("REGION_ID") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap() +}); + +pub static REGION_KEY: Lazy = Lazy::new(|| format!("region{}", &*REGION_ID)); +pub static ONLINE_SET: &str = "online"; + +pub static FLAG_BITS: u32 = 0b1; /// Create a new presence session, returns the ID of this session -pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u32) { +pub async fn create_session(user_id: &str, flags: u8) -> (bool, u32) { info!("Creating a presence session for {user_id} with flags {flags}"); if let Ok(mut conn) = get_connection().await { @@ -24,7 +35,7 @@ pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u32) { // A session ID is comprised of random data and any flags ORed to the end let session_id = { let mut rng = rand::thread_rng(); - (rng.gen::() ^ 1) | (flags as u32 & 1) + (rng.gen::() & !FLAG_BITS) | (flags as u32 & FLAG_BITS) }; // Add session to user's sessions and to the region @@ -41,16 +52,12 @@ pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u32) { } /// Delete existing presence session -pub async fn presence_delete_session(user_id: &str, session_id: u32) -> bool { - presence_delete_session_internal(user_id, session_id, false).await +pub async fn delete_session(user_id: &str, session_id: u32) -> bool { + delete_session_internal(user_id, session_id, false).await } /// Delete existing presence session (but also choose whether to skip region) -async fn presence_delete_session_internal( - user_id: &str, - session_id: u32, - skip_region: bool, -) -> bool { +async fn delete_session_internal(user_id: &str, session_id: u32, skip_region: bool) -> bool { info!("Deleting presence session for {user_id} with id {session_id}"); if let Ok(mut conn) = get_connection().await { @@ -78,7 +85,7 @@ async fn presence_delete_session_internal( } /// Check whether a given user ID is online -pub async fn presence_is_online(user_id: &str) -> bool { +pub async fn is_online(user_id: &str) -> bool { if let Ok(mut conn) = get_connection().await { conn.exists(user_id).await.unwrap_or(false) } else { @@ -87,7 +94,7 @@ pub async fn presence_is_online(user_id: &str) -> bool { } /// Check whether a set of users is online, returns a set of the online user IDs -pub async fn presence_filter_online(user_ids: &'_ [String]) -> HashSet { +pub async fn filter_online(user_ids: &'_ [String]) -> HashSet { // Ignore empty list immediately, to save time. let mut set = HashSet::new(); if user_ids.is_empty() { @@ -102,7 +109,7 @@ pub async fn presence_filter_online(user_ids: &'_ [String]) -> HashSet { // as for some reason or another, Redis does not like us sending // a list of just one ID to the server. if user_ids.len() == 1 { - if presence_is_online(&user_ids[0]).await { + if is_online(&user_ids[0]).await { set.insert(user_ids[0].to_string()); } @@ -133,7 +140,7 @@ pub async fn presence_filter_online(user_ids: &'_ [String]) -> HashSet { } /// Reset any stale presence data -pub async fn presence_clear_region(region_id: Option<&str>) { +pub async fn clear_region(region_id: Option<&str>) { let region_id = region_id.unwrap_or(&*REGION_KEY); let mut conn = get_connection().await.expect("Redis connection"); @@ -150,7 +157,7 @@ pub async fn presence_clear_region(region_id: Option<&str>) { let parts = session.split(':').collect::>(); if let (Some(user_id), Some(session_id)) = (parts.first(), parts.get(1)) { if let Ok(session_id) = session_id.parse() { - presence_delete_session_internal(user_id, session_id, true).await; + delete_session_internal(user_id, session_id, true).await; } } } @@ -161,3 +168,59 @@ pub async fn presence_clear_region(region_id: Option<&str>) { info!("Clean up complete."); } } + +#[cfg(test)] +mod tests { + use crate::{clear_region, create_session, delete_session, filter_online, is_online}; + use rand::Rng; + + #[async_std::test] + async fn it_works() { + // Clear the region before we start the tests: + clear_region(None).await; + + // Generate some data we'll use: + let user_id = rand::thread_rng().gen::().to_string(); + let other_id = rand::thread_rng().gen::().to_string(); + let flags = 1; + + // Create a session + let (first_session, session_id) = create_session(&user_id, flags).await; + assert!(first_session); + assert_ne!(session_id, 0); + assert_eq!(session_id as u8 & flags, flags); + + // Check if the user is online + assert!(is_online(&user_id).await); + + let user_ids = filter_online(&[user_id.to_string()]).await; + assert_eq!(user_ids.len(), 1); + assert!(user_ids.contains(&user_id)); + + // Create a few more sessions + let (first_session, second_session_id) = create_session(&user_id, 0).await; + assert!(!first_session); + dbg!(second_session_id); + assert_eq!(second_session_id as u8 & 1, 0); + + let (first_session, other_session_id) = create_session(&other_id, 0).await; + assert!(first_session); + + let user_ids = filter_online(&[user_id.to_string(), other_id.to_string()]).await; + assert_eq!(user_ids.len(), 2); + assert!(user_ids.contains(&user_id)); + assert!(user_ids.contains(&other_id)); + + // Remove sessions + delete_session(&user_id, session_id).await; + delete_session(&other_id, other_session_id).await; + assert!(!is_online(&other_id).await); + + // Check if we can wipe everything too + clear_region(None).await; + assert!(!is_online(&user_id).await); + + let user_ids = filter_online(&[user_id.to_string(), other_id.to_string()]).await; + assert!(user_ids.is_empty()) + } +} diff --git a/crates/quark/src/presence/operations.rs b/crates/core/presence/src/operations.rs similarity index 100% rename from crates/quark/src/presence/operations.rs rename to crates/core/presence/src/operations.rs diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml new file mode 100644 index 00000000..5f3820db --- /dev/null +++ b/crates/core/result/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "revolt-result" +version = "0.0.1" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = [ "Paul Makles " ] +description = "Revolt Backend: Result and Error types" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +serde = [ "dep:serde" ] +schemas = [ "dep:schemars" ] + +default = [ "serde" ] + +[dependencies] +# Serialisation +serde = { version = "1", features = ["derive"], optional = true } + +# Spec Generation +schemars = { version = "0.8.8", optional = true } diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs new file mode 100644 index 00000000..7e0122da --- /dev/null +++ b/crates/core/result/src/lib.rs @@ -0,0 +1,178 @@ +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde; + +#[cfg(feature = "schemas")] +#[macro_use] +extern crate schemars; + +/// Result type with custom Error +pub type Result = std::result::Result; + +/// Error information +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "schemas", derive(JsonSchema))] +#[derive(Debug, Clone)] +pub struct Error { + /// Type of error and additional information + #[cfg_attr(feature = "serde", serde(flatten))] + pub error_type: ErrorType, + + /// Where this error occurred + pub location: String, +} + +/// Possible error types +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(tag = "type"))] +#[cfg_attr(feature = "schemas", derive(JsonSchema))] +#[derive(Debug, Clone)] +pub enum ErrorType { + /// This error was not labeled :( + LabelMe, + + // ? Onboarding related errors + AlreadyOnboarded, + + // ? User related errors + UsernameTaken, + InvalidUsername, + UnknownUser, + AlreadyFriends, + AlreadySentRequest, + Blocked, + BlockedByOther, + NotFriends, + + // ? Channel related errors + UnknownChannel, + UnknownAttachment, + UnknownMessage, + CannotEditMessage, + CannotJoinCall, + TooManyAttachments { + max: usize, + }, + TooManyReplies { + max: usize, + }, + TooManyChannels { + max: usize, + }, + EmptyMessage, + PayloadTooLarge, + CannotRemoveYourself, + GroupTooLarge { + max: usize, + }, + AlreadyInGroup, + NotInGroup, + + // ? Server related errors + UnknownServer, + InvalidRole, + Banned, + TooManyServers { + max: usize, + }, + TooManyEmoji { + max: usize, + }, + TooManyRoles { + max: usize, + }, + + // ? Bot related errors + ReachedMaximumBots, + IsBot, + BotIsPrivate, + + // ? User safety related errors + CannotReportYourself, + + // ? Permission errors + MissingPermission { + permission: String, + }, + MissingUserPermission { + permission: String, + }, + NotElevated, + NotPrivileged, + CannotGiveMissingPermissions, + NotOwner, + + // ? General errors + DatabaseError { + operation: String, + collection: String, + }, + InternalError, + InvalidOperation, + InvalidCredentials, + InvalidProperty, + InvalidSession, + DuplicateNonce, + NotFound, + NoEffect, + FailedValidation { + error: String, + }, + + // ? Legacy errors + VosoUnavailable, +} + +#[macro_export] +macro_rules! create_error { + ( $error: ident $( $tt:tt )? ) => { + $crate::Error { + error_type: $crate::ErrorType::$error $( $tt )?, + location: format!("{}:{}:{}", file!(), line!(), column!()), + } + }; +} + +#[macro_export] +macro_rules! create_database_error { + ( $operation: expr, $collection: expr ) => { + create_error!(DatabaseError { + operation: $operation.to_string(), + collection: $collection.to_string() + }) + }; +} + +#[macro_export] +#[cfg(debug_assertions)] +macro_rules! query { + ( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => { + Ok($self.$type($collection, $($rest),+).await.unwrap()) + }; +} + +#[macro_export] +#[cfg(not(debug_assertions))] +macro_rules! query { + ( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => { + $self.$type($collection, $($rest),+).await + .map_err(|_| create_database_error!(stringify!($type), $collection)) + }; +} + +#[cfg(test)] +mod tests { + use crate::ErrorType; + + #[test] + fn use_macro_to_construct_error() { + let error = create_error!(LabelMe); + assert!(matches!(error.error_type, ErrorType::LabelMe)); + } + + #[test] + fn use_macro_to_construct_complex_error() { + let error = create_error!(LabelMe); + assert!(matches!(error.error_type, ErrorType::LabelMe)); + } +} diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 84d4ffdd..b4c7cde6 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -55,5 +55,9 @@ revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] } # quark revolt-quark = { path = "../quark" } +# core +revolt-database = { path = "../core/database" } +revolt-models = { path = "../core/models", features = [ "schemas" ] } + [build-dependencies] vergen = "7.5.0" diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 4dc3ba38..e88897d1 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -22,15 +22,18 @@ async fn rocket() -> _ { revolt_quark::variables::delta::preflight_checks(); // Setup database - let db = DatabaseInfo::Auto.connect().await.unwrap(); + let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); db.migrate_database().await.unwrap(); + // Legacy database setup from quark + let legacy_db = DatabaseInfo::Auto.connect().await.unwrap(); + // Setup Authifier event channel let (sender, receiver) = unbounded(); // Setup Authifier let authifier = Authifier { - database: db.clone().into(), + database: legacy_db.clone().into(), config: revolt_quark::util::authifier::config(), event_channel: Some(sender), }; @@ -52,7 +55,7 @@ async fn rocket() -> _ { }); // Launch background task workers - async_std::task::spawn(revolt_quark::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(); @@ -65,6 +68,7 @@ async fn rocket() -> _ { .mount("/swagger/", revolt_quark::web::swagger::routes()) .manage(authifier) .manage(db) + .manage(legacy_db) .manage(cors.clone()) .attach(revolt_quark::web::ratelimiter::RatelimitFairing) .attach(cors) diff --git a/crates/delta/src/routes/bots/fetch.rs b/crates/delta/src/routes/bots/fetch.rs index a6f94a4f..19a16ee0 100644 --- a/crates/delta/src/routes/bots/fetch.rs +++ b/crates/delta/src/routes/bots/fetch.rs @@ -1,11 +1,11 @@ -use revolt_quark::{ - models::{Bot, User}, - Db, Error, Ref, Result, -}; -use rocket::serde::json::Json; +use revolt_database::Database; +use revolt_models::Bot; +use revolt_quark::{models::User, Db, Error, Ref, Result}; +use rocket::{serde::json::Json, State}; use serde::Serialize; /// # Bot Response +/// TODO: move to revolt-models #[derive(Serialize, JsonSchema)] pub struct BotResponse { /// Bot object @@ -19,18 +19,23 @@ pub struct BotResponse { /// Fetch details of a bot you own by its id. #[openapi(tag = "Bots")] #[get("/")] -pub async fn fetch_bot(db: &Db, user: User, target: Ref) -> Result> { +pub async fn fetch_bot( + legacy_db: &Db, + db: &State, + user: User, + target: Ref, +) -> Result> { if user.bot.is_some() { return Err(Error::IsBot); } - let bot = target.as_bot(db).await?; + let bot = db.fetch_bot(&target.id).await.map_err(Error::from_core)?; if bot.owner != user.id { return Err(Error::NotFound); } Ok(Json(BotResponse { - user: db.fetch_user(&bot.id).await?.foreign(), - bot, + user: legacy_db.fetch_user(&bot.id).await?.foreign(), + bot: bot.into(), })) } diff --git a/crates/delta/src/routes/bots/fetch_public.rs b/crates/delta/src/routes/bots/fetch_public.rs index 03e80a65..1473eb65 100644 --- a/crates/delta/src/routes/bots/fetch_public.rs +++ b/crates/delta/src/routes/bots/fetch_public.rs @@ -1,44 +1,32 @@ -use revolt_quark::{ - models::{File, User}, - Db, Error, Ref, Result, -}; +use revolt_database::Database; +use revolt_models::PublicBot; +use revolt_quark::{models::User, Db, Error, Ref, Result}; use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; - -/// # Public Bot -#[derive(Serialize, Deserialize, JsonSchema)] -pub struct PublicBot { - /// Bot Id - #[serde(rename = "_id")] - id: String, - /// Bot Username - username: String, - /// Profile Avatar - #[serde(skip_serializing_if = "Option::is_none")] - avatar: Option, - /// Profile Description - #[serde(skip_serializing_if = "Option::is_none")] - description: Option, -} +use rocket::State; /// # Fetch Public Bot /// /// Fetch details of a public (or owned) bot by its id. #[openapi(tag = "Bots")] #[get("//invite")] -pub async fn fetch_public_bot(db: &Db, user: Option, target: Ref) -> Result> { - let bot = target.as_bot(db).await?; +pub async fn fetch_public_bot( + legacy_db: &Db, + db: &State, + user: Option, + target: Ref, +) -> Result> { + let bot = db.fetch_bot(&target.id).await.map_err(Error::from_core)?; if !bot.public && user.map_or(true, |x| x.id != bot.owner) { return Err(Error::NotFound); } - let user = db.fetch_user(&bot.id).await?; + let user = legacy_db.fetch_user(&bot.id).await?; - Ok(Json(PublicBot { - id: bot.id, - username: user.username, - avatar: user.avatar, - description: user.profile.and_then(|p| p.content), - })) + Ok(Json(PublicBot::from( + bot, + user.username, + user.avatar.map(|f| f.id), + user.profile.and_then(|p| p.content), + ))) } diff --git a/crates/quark/Cargo.toml b/crates/quark/Cargo.toml index 43a22a9d..06f14a96 100644 --- a/crates/quark/Cargo.toml +++ b/crates/quark/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "revolt-quark" version = "0.5.19" -license = "AGPL-3.0-or-later" edition = "2021" +license = "AGPL-3.0-or-later" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -26,10 +26,10 @@ default = [ "test" ] [dependencies] # Serialisation +revolt_optional_struct = "0.2.0" serde = { version = "1", features = ["derive"] } validator = { version = "0.14", features = ["derive"] } iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] } -optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "ee56427cee1f007839825d93d07fffd5a5e038c7" } # Formats bincode = "1.3.3" @@ -89,3 +89,7 @@ authifier = { version = "1.0.7", features = [ "async-std-runtime" ] } # Sentry sentry = "0.25.0" + +# Core +revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] } +revolt-presence = { path = "../core/presence" } diff --git a/crates/quark/examples/test.rs b/crates/quark/examples/test.rs deleted file mode 100644 index d09220c3..00000000 --- a/crates/quark/examples/test.rs +++ /dev/null @@ -1,34 +0,0 @@ -use revolt_quark::models::user::PartialUser; -use revolt_quark::presence::{ - presence_create_session, presence_delete_session, presence_filter_online, presence_is_online, -}; -use revolt_quark::*; - -#[async_std::main] -async fn main() { - let db = DatabaseInfo::Dummy.connect().await.unwrap(); - - let sus = PartialUser { - username: Some("neat".into()), - ..Default::default() - }; - - db.update_user("user id", &sus, vec![]).await.unwrap(); - - dbg!(presence_create_session("entry", 0).await); - dbg!(presence_is_online("entry").await); - dbg!(presence_filter_online(&["a".into(), "b".into(), "entry".into()]).await); - - dbg!(presence_delete_session("entry", 0).await); - dbg!(presence_is_online("entry").await); - - dbg!(presence_create_session("entry", 0).await); - dbg!(presence_create_session("entry", 0).await); - dbg!(presence_delete_session("entry", 0).await); - dbg!(presence_is_online("entry").await); - dbg!(presence_delete_session("entry", 1).await); - dbg!(presence_is_online("entry").await); - - // __set_key("dietz", vec![0xFF]).await; - // dbg!(presence_filter_online(&["dietz".into(), "nuts".into()]).await); -} diff --git a/crates/quark/src/events/impl.rs b/crates/quark/src/events/impl.rs index ffac793e..0ea6e05b 100644 --- a/crates/quark/src/events/impl.rs +++ b/crates/quark/src/events/impl.rs @@ -7,11 +7,11 @@ use crate::{ user::{PartialUser, Presence, RelationshipStatus}, Channel, Member, User, }, - perms, - presence::presence_filter_online, - Database, Permission, Result, + perms, Database, Permission, Result, }; +use revolt_presence::filter_online; + use super::{ client::EventV1, state::{Cache, State}, @@ -135,8 +135,7 @@ impl State { } // Fetch presence data for known users. - let online_ids = - presence_filter_online(&user_ids.iter().cloned().collect::>()).await; + let online_ids = filter_online(&user_ids.iter().cloned().collect::>()).await; user.online = Some(true); // Fetch user data. diff --git a/crates/quark/src/impl/dummy/admin/migrations.rs b/crates/quark/src/impl/dummy/admin/migrations.rs deleted file mode 100644 index d8694453..00000000 --- a/crates/quark/src/impl/dummy/admin/migrations.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{AbstractMigrations, Result}; - -use super::super::DummyDb; - -#[async_trait] -impl AbstractMigrations for DummyDb { - async fn migrate_database(&self) -> Result<()> { - info!("Migrating the database."); - Ok(()) - } -} diff --git a/crates/quark/src/impl/dummy/mod.rs b/crates/quark/src/impl/dummy/mod.rs index fe26641b..194f2edb 100644 --- a/crates/quark/src/impl/dummy/mod.rs +++ b/crates/quark/src/impl/dummy/mod.rs @@ -1,7 +1,6 @@ use crate::AbstractDatabase; pub mod admin { - pub mod migrations; pub mod stats; } diff --git a/crates/quark/src/impl/generic/admin/migrations.rs b/crates/quark/src/impl/generic/admin/migrations.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/quark/src/impl/generic/admin/migrations.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/quark/src/impl/generic/channels/message.rs b/crates/quark/src/impl/generic/channels/message.rs index aaee8e2d..308285a1 100644 --- a/crates/quark/src/impl/generic/channels/message.rs +++ b/crates/quark/src/impl/generic/channels/message.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use revolt_presence::filter_online; use serde_json::json; use ulid::Ulid; use validator::Validate; @@ -14,7 +15,6 @@ use crate::{ Channel, Emoji, Message, User, }, permissions::PermissionCalculator, - presence::presence_filter_online, tasks::ack::AckEvent, types::{ january::{Embed, Text}, @@ -79,7 +79,7 @@ impl Message { Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => { target_ids = (&recipients.iter().cloned().collect::>() - - &presence_filter_online(recipients).await) + - &filter_online(recipients).await) .into_iter() .collect::>(); } diff --git a/crates/quark/src/impl/generic/mod.rs b/crates/quark/src/impl/generic/mod.rs index 5dd5dff6..015bc9dd 100644 --- a/crates/quark/src/impl/generic/mod.rs +++ b/crates/quark/src/impl/generic/mod.rs @@ -1,9 +1,5 @@ //! Database agnostic implementations. -pub mod admin { - pub mod migrations; -} - pub mod media { pub mod attachment; pub mod emoji; diff --git a/crates/quark/src/impl/generic/users/user.rs b/crates/quark/src/impl/generic/users/user.rs index 76d2603b..2707723b 100644 --- a/crates/quark/src/impl/generic/users/user.rs +++ b/crates/quark/src/impl/generic/users/user.rs @@ -4,11 +4,11 @@ use crate::models::user::{ }; use crate::permissions::defn::UserPerms; use crate::permissions::r#impl::user::get_relationship; -use crate::presence::presence_filter_online; use crate::{perms, Database, Error, Result}; use futures::try_join; use impl_ops::impl_op_ex_commutative; +use revolt_presence::filter_online; use std::ops; impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 }); @@ -98,7 +98,7 @@ impl User { /// Fetch foreign users by a list of IDs pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result> { - let online_ids = presence_filter_online(user_ids).await; + let online_ids = filter_online(user_ids).await; Ok(db .fetch_users(user_ids) diff --git a/crates/quark/src/impl/mongo/mod.rs b/crates/quark/src/impl/mongo/mod.rs index 582d3d6e..7711ab0f 100644 --- a/crates/quark/src/impl/mongo/mod.rs +++ b/crates/quark/src/impl/mongo/mod.rs @@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize}; use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result}; pub mod admin { - pub mod migrations; pub mod stats; } diff --git a/crates/quark/src/lib.rs b/crates/quark/src/lib.rs index 13b1ce82..b95c6583 100644 --- a/crates/quark/src/lib.rs +++ b/crates/quark/src/lib.rs @@ -9,7 +9,7 @@ extern crate log; #[macro_use] extern crate impl_ops; #[macro_use] -extern crate optional_struct; +extern crate revolt_optional_struct; #[macro_use] extern crate bitfield; #[macro_use] @@ -22,7 +22,6 @@ pub use redis_kiss; pub mod events; pub mod r#impl; pub mod models; -pub mod presence; pub mod tasks; pub mod types; pub mod util; diff --git a/crates/quark/src/models/admin/migrations.rs b/crates/quark/src/models/admin/migrations.rs deleted file mode 100644 index 0a3e620f..00000000 --- a/crates/quark/src/models/admin/migrations.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Document representing migration information -#[derive(Serialize, Deserialize)] -pub struct MigrationInfo { - /// Unique Id - #[serde(rename = "_id")] - id: i32, - /// Current database revision - revision: i32, -} diff --git a/crates/quark/src/models/mod.rs b/crates/quark/src/models/mod.rs index c0fdcf28..370c72a1 100644 --- a/crates/quark/src/models/mod.rs +++ b/crates/quark/src/models/mod.rs @@ -1,5 +1,4 @@ mod admin { - pub mod migrations; pub mod simple; pub mod stats; } @@ -47,7 +46,6 @@ pub use channel_invite::Invite; pub use channel_unread::ChannelUnread; pub use emoji::Emoji; pub use message::Message; -pub use migrations::MigrationInfo; pub use report::Report; pub use server::Server; pub use server_ban::ServerBan; diff --git a/crates/quark/src/presence/entry.rs b/crates/quark/src/presence/entry.rs deleted file mode 100644 index c177a5ab..00000000 --- a/crates/quark/src/presence/entry.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::env; - -use once_cell::sync::Lazy; - -pub static REGION_ID: Lazy = Lazy::new(|| { - env::var("REGION_ID") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap() -}); - -pub static REGION_KEY: Lazy = Lazy::new(|| format!("region{}", &*REGION_ID)); -pub static ONLINE_SET: &str = "online"; diff --git a/crates/quark/src/traits/admin/migrations.rs b/crates/quark/src/traits/admin/migrations.rs deleted file mode 100644 index bb46db7a..00000000 --- a/crates/quark/src/traits/admin/migrations.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::Result; - -#[async_trait] -pub trait AbstractMigrations: Sync + Send { - async fn migrate_database(&self) -> Result<()>; -} diff --git a/crates/quark/src/traits/mod.rs b/crates/quark/src/traits/mod.rs index b7c2f6c9..341565a1 100644 --- a/crates/quark/src/traits/mod.rs +++ b/crates/quark/src/traits/mod.rs @@ -1,5 +1,4 @@ mod admin { - pub mod migrations; pub mod stats; } @@ -32,7 +31,6 @@ mod safety { pub mod snapshot; } -pub use admin::migrations::AbstractMigrations; pub use admin::stats::AbstractStats; pub use media::attachment::AbstractAttachment; @@ -57,7 +55,6 @@ pub use safety::snapshot::AbstractSnapshot; pub trait AbstractDatabase: Sync + Send - + AbstractMigrations + AbstractStats + AbstractAttachment + AbstractEmoji diff --git a/crates/quark/src/util/ref.rs b/crates/quark/src/util/ref.rs index 4d59bc06..1b3ff148 100644 --- a/crates/quark/src/util/ref.rs +++ b/crates/quark/src/util/ref.rs @@ -1,4 +1,5 @@ use futures::future::join; +use revolt_presence::is_online; use rocket::request::FromParam; use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; use schemars::JsonSchema; @@ -7,7 +8,6 @@ use serde::{Deserialize, Serialize}; use crate::models::{ Bot, Channel, Emoji, Invite, Member, Message, Report, Server, ServerBan, User, }; -use crate::presence::presence_is_online; use crate::{Database, Error, Result}; /// Reference to some object in the database @@ -25,7 +25,7 @@ impl Ref { /// Fetch user from Ref pub async fn as_user(&self, db: &Database) -> Result { - let (user, online) = join(db.fetch_user(&self.id), presence_is_online(&self.id)).await; + let (user, online) = join(db.fetch_user(&self.id), is_online(&self.id)).await; let mut user = user?; user.online = Some(online); Ok(user) diff --git a/crates/quark/src/util/result.rs b/crates/quark/src/util/result.rs index 8fb449d0..a717d999 100644 --- a/crates/quark/src/util/result.rs +++ b/crates/quark/src/util/result.rs @@ -19,6 +19,12 @@ pub enum Error { /// This error was not labeled :( LabelMe, + /// Core crate error + Core { + #[serde(flatten)] + error: revolt_result::Error, + }, + // ? Onboarding related errors AlreadyOnboarded, @@ -39,13 +45,13 @@ pub enum Error { CannotEditMessage, CannotJoinCall, TooManyAttachments { - max: usize + max: usize, }, TooManyReplies { - max: usize + max: usize, }, TooManyChannels { - max: usize + max: usize, }, EmptyMessage, PayloadTooLarge, @@ -64,10 +70,10 @@ pub enum Error { max: usize, }, TooManyEmoji { - max: usize + max: usize, }, TooManyRoles { - max: usize + max: usize, }, // ? Bot related errors @@ -135,6 +141,11 @@ impl Error { error: validation_error, }) } + + /// Create a error from core error + pub fn from_core(error: revolt_result::Error) -> Error { + Error::Core { error } + } } /// Result type with custom Error @@ -145,6 +156,7 @@ impl<'r> Responder<'r, 'static> for Error { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { let status = match self { Error::LabelMe => Status::InternalServerError, + Error::Core { .. } => Status::InternalServerError, Error::AlreadyOnboarded => Status::Forbidden, diff --git a/justfile b/justfile new file mode 100644 index 00000000..778aeaa4 --- /dev/null +++ b/justfile @@ -0,0 +1,4 @@ +publish: + cargo publish --package revolt-result + cargo publish --package revolt-database + cargo publish --package revolt-models