commit e27a0d33d75f7f856fa0cb71d29d9538261f2417 Author: CanadianBaconBoi Date: Tue Feb 17 18:20:02 2026 +0100 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cove-chat.iml b/.idea/cove-chat.iml new file mode 100644 index 0000000..838d228 --- /dev/null +++ b/.idea/cove-chat.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..7fabf9c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..3840144 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fd142e5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2809 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bin-test" +version = "0.1.0" +dependencies = [ + "anyhow", + "cove-db", + "cove-net-common", + "cove-net-server", + "scc", + "sqlx", + "tokio", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cove-db" +version = "0.1.0" +dependencies = [ + "anyhow", + "cove-net-common", + "serde_json", + "sqlx", +] + +[[package]] +name = "cove-net-client" +version = "0.1.0" + +[[package]] +name = "cove-net-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "rand 0.10.0", + "serde", + "serde_json", + "serde_with", + "sqlx", +] + +[[package]] +name = "cove-net-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "cove-db", + "cove-net-common", + "http-body-util", + "hyper", + "hyper-util", + "scc", + "serde_json", + "tokio", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.1", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "saa" +version = "5.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0ba8adb63e0deebd0744d8fc5bea394c08029159deaf680513fec1a3949144" + +[[package]] +name = "scc" +version = "3.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bd9d1727de391b6982925d830baad51692fa2aa6e337733c03d95121ca2793" +dependencies = [ + "saa", + "sdd", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c25da4ae64b24edfcb0b0d30b96b2b0dbc64ec63aefeb6ec35bfc5ef167e5c9e" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..16a3319 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] +resolver = "3" +members = [ + "cove-net/common", + "cove-net/client", + "cove-net/server" +, "bin-test", "cove-db"] + +[workspace.dependencies] + +scc = "3.5.6" +async-trait = "0.1.89" +anyhow = "1.0.101" +hyper = { version = "1", features = ["full"] } +http-body-util = { version = "0.1.3", features = ["full"] } + +cove-net-common = {path = "cove-net/common"} +serde_json = "1.0.149" +serde_with = "3.16.1" +serde = { version = "1.0.228", features = ["derive"] } +hex = "0.4.3" + +cove-net-server = {path = "cove-net/server"} +tokio = { version = "1", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } + +cove-db = {path = "cove-db"} +sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]} \ No newline at end of file diff --git a/bin-test/Cargo.toml b/bin-test/Cargo.toml new file mode 100644 index 0000000..9bb4d34 --- /dev/null +++ b/bin-test/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bin-test" +version = "0.1.0" +edition = "2024" + +[dependencies] +cove-net-server.workspace = true +cove-net-common.workspace = true +tokio.workspace = true +cove-db.workspace = true +sqlx.workspace = true +anyhow = "1.0.101" +scc = "3.5.6" \ No newline at end of file diff --git a/bin-test/migrations/01_user.sql b/bin-test/migrations/01_user.sql new file mode 100644 index 0000000..9af56bc --- /dev/null +++ b/bin-test/migrations/01_user.sql @@ -0,0 +1,30 @@ +-- user status enum values +DO $$ BEGIN + CREATE TYPE user_status AS ENUM ('online', 'idle', 'dnd', 'offline', 'invisible'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + + +-- main users table +CREATE TABLE users ( + id BYTEA PRIMARY KEY NOT NULL CHECK(length(id) = 26), + username TEXT NOT NULL CHECK (char_length(username) BETWEEN 1 AND 32), + discriminator TEXT NOT NULL DEFAULT '0000' CHECK (discriminator ~ '^\d{4}$'), + avatar_hash TEXT CHECK (char_length(avatar_hash) <= 64), -- null if no avatar + email TEXT UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + password_hash TEXT NOT NULL, -- bcrypt/argon2 hash, never plaintext + mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE, + mfa_secret TEXT CHECK (mfa_enabled = FALSE OR char_length(mfa_secret) > 0), -- encrypted if present + status user_status NOT NULL DEFAULT 'offline', + public_flags BIGINT NOT NULL DEFAULT 0, -- bitmask for flags + locale TEXT NOT NULL DEFAULT 'en-US' CHECK (locale ~ '^[a-z]{2}(-[A-Z]{2})?$'), -- BCP 47 pattern + premium_since TIMESTAMPTZ, -- e.g., Premium subscription start + premium_end TIMESTAMPTZ, + bot BOOLEAN NOT NULL DEFAULT FALSE, + bot_oauth_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- e.g., ['bot', 'guilds.join'] + preferences JSONB NOT NULL DEFAULT '{}'::jsonb, -- theme, sound, etc. + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/bin-test/migrations/02_messages.sql b/bin-test/migrations/02_messages.sql new file mode 100644 index 0000000..80a3e14 --- /dev/null +++ b/bin-test/migrations/02_messages.sql @@ -0,0 +1,43 @@ +CREATE TABLE messages ( + id BYTEA NOT NULL CHECK(length(id) = 26), + channel_id BYTEA NOT NULL CHECK(length(channel_id) = 26), + guild_id BYTEA CHECK(length(guild_id) = 26), -- nullable for DMs + author_id BYTEA NOT NULL CHECK(length(author_id) = 26), + + -- Message content and metadata + content TEXT NOT NULL DEFAULT '', + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + edited_timestamp TIMESTAMPTZ, + tts BOOLEAN NOT NULL DEFAULT FALSE, + + -- Mentions and embeds (as JSON for flexibility) + mentions BYTEA[], -- array of user/channel/role IDs (could also be TEXT[] if using base58) + mention_everyone BOOLEAN NOT NULL DEFAULT FALSE, + embeds JSONB[], + attachments BYTEA[], + + -- Reply/reference data (optional) + reply_message_id BYTEA CHECK(length(reply_message_id) = 26), + application_id BYTEA CHECK(length(application_id) = 26), -- for slash commands + + -- System/interaction message type + message_type INTEGER NOT NULL DEFAULT 0, -- 0 = default, 1 = reply, 2 = gateway ping, etc. + + -- Thread support + thread_name TEXT, + auto_archive_duration INTEGER, -- minutes (360, 1440, 4320, 10080) + + -- Audit & integrity + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (id) +); + +-- Indexes for common query patterns +CREATE INDEX idx_messages_channel_id ON messages (channel_id); +CREATE INDEX idx_messages_channel_id_timestamp ON messages (channel_id, timestamp DESC); +CREATE INDEX idx_messages_author_id ON messages (author_id); +CREATE INDEX idx_messages_guild_id ON messages (guild_id) WHERE guild_id IS NOT NULL; +CREATE INDEX idx_messages_reference_message_id ON messages (reply_message_id); +CREATE INDEX idx_messages_timestamp ON messages (timestamp DESC); diff --git a/bin-test/migrations/03_nonce.sql b/bin-test/migrations/03_nonce.sql new file mode 100644 index 0000000..c018242 --- /dev/null +++ b/bin-test/migrations/03_nonce.sql @@ -0,0 +1,24 @@ +-- Staging table for pending nonces (with TTL) +CREATE TABLE pending_nonces ( + nonce TEXT NOT NULL PRIMARY KEY, + channel_id BYTEA NOT NULL CHECK (LENGTH(channel_id) = 26), + author_id BYTEA NOT NULL CHECK (LENGTH(author_id) = 26), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL +); + +-- Index for expiry queries +CREATE INDEX idx_pending_nonces_expires_at ON pending_nonces (expires_at); + +CREATE OR REPLACE FUNCTION set_nonce_expiry() + RETURNS TRIGGER AS $$ +BEGIN + NEW.expires_at := NOW() + INTERVAL '5 minutes'; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trig_pending_nonces_expiry + BEFORE INSERT ON pending_nonces + FOR EACH ROW +EXECUTE FUNCTION set_nonce_expiry(); \ No newline at end of file diff --git a/bin-test/migrations/04_channels.sql b/bin-test/migrations/04_channels.sql new file mode 100644 index 0000000..8fe1355 --- /dev/null +++ b/bin-test/migrations/04_channels.sql @@ -0,0 +1,44 @@ +CREATE TABLE channels ( + id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26), + guild_id BYTEA CHECK(length(guild_id) = 26), -- NULL for DMs & group DMs + parent_id BYTEA CHECK(length(parent_id) = 26), -- for thread categories / channel groups + + -- Core identity & type + name TEXT NOT NULL CHECK (LENGTH(name) >= 1 AND LENGTH(name) <= 100), + channel_type INTEGER NOT NULL DEFAULT 0, -- 0 = text, 1 = voice, 2 = category, 5 = news, etc. + + -- Permissions & visibility + position INTEGER NOT NULL DEFAULT 0, + permission_overwrites JSONB NOT NULL DEFAULT '[]'::jsonb, -- array of overwrites + rate_limit_per_user INTEGER NOT NULL DEFAULT 0, -- slowmode in seconds (0–21600) + + -- NSFW & visibility flags + nsfw BOOLEAN NOT NULL DEFAULT FALSE, + loud BOOLEAN NOT NULL DEFAULT FALSE, -- voice channel: triggers notifications + + -- Thread-specific fields (for threads spawned from messages) + thread_metadata JSONB, -- { "archived": bool, "auto_archive_duration": int, "archive_timestamp": timestamptz, "locked": bool } + member_count INTEGER NOT NULL DEFAULT 0, -- approximate member count (not real-time) + message_count INTEGER NOT NULL DEFAULT 0, -- cached message count (for UI previews) + thread_owner_id BYTEA CHECK(length(thread_owner_id) = 26), + + -- System metadata + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Topic (for news, text channels) + topic TEXT CHECK (LENGTH(COALESCE(topic, '')) <= 1024), + + -- Voice-specific + user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = no limit + region TEXT -- voice region (e.g., "us-west"), NULL = automatic +); + +-- Critical indexes for common operations +CREATE INDEX idx_channels_guild_id ON channels (guild_id) WHERE guild_id IS NOT NULL; +CREATE INDEX idx_channels_guild_id_position ON channels (guild_id, position); +CREATE INDEX idx_channels_parent_id ON channels (parent_id) WHERE parent_id IS NOT NULL; +CREATE INDEX idx_channels_channel_type ON channels (channel_type); + +-- Optional: cover index for message listing (join + sort) +CREATE INDEX idx_channels_guild_id_created_at ON channels (guild_id, created_at) WHERE guild_id IS NOT NULL; \ No newline at end of file diff --git a/bin-test/migrations/05_guilds.sql b/bin-test/migrations/05_guilds.sql new file mode 100644 index 0000000..5f01772 --- /dev/null +++ b/bin-test/migrations/05_guilds.sql @@ -0,0 +1,56 @@ +CREATE TABLE guilds ( + id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26), + + -- Core identity + name TEXT NOT NULL CHECK (LENGTH(name) >= 2 AND LENGTH(name) <= 100), + description TEXT CHECK (LENGTH(COALESCE(description, '')) <= 1024), + icon BYTEA CHECK(length(icon) = 26), + banner BYTEA CHECK(length(banner) = 26), + splash BYTEA CHECK(length(splash) = 26), + + -- Ownership & verification + owner_id BYTEA NOT NULL CHECK(length(owner_id) = 26), + owner_permissions BYTEA NOT NULL DEFAULT 'xFFFFFFFFFFFFFFFF'::BYTEA, -- 8-byte bitmask (e.g., ADMINISTRATOR = 0x8) + + -- Regions & voice + region TEXT NOT NULL DEFAULT 'us-west', -- voice region (e.g., 'us-west', 'eu-central') + + -- Features (bitmask of enabled features) + features INTEGER NOT NULL DEFAULT 0, -- 0 = basic, 1 = ANIMATED_ICON, 2 = BANNER, 4 = COMMERCE, 8 = PUBLIC, etc. + + -- Discovery & visibility + afk_channel_id BYTEA CHECK(length(afk_channel_id) = 26), + afk_timeout INTEGER NOT NULL DEFAULT 300, -- seconds (60, 300, 900, 1800, 3600) + verification_level INTEGER NOT NULL DEFAULT 0, -- 0 = none, 1 = low, 2 = medium, 3 = high, 4 = highest + default_message_notifications INTEGER NOT NULL DEFAULT 1, -- 0 = all, 1 = mentions only + + -- Explicit content filter + explicit_content_filter INTEGER NOT NULL DEFAULT 0, -- 0 = disabled, 1 = members without role, 2 = all + + -- System channels + system_channel_id BYTEA CHECK(length(system_channel_id) = 26), + system_channel_flags INTEGER NOT NULL DEFAULT 0, -- 1 = SUPPRESS_JOIN_NOTIFICATIONS, 2 = SUPPRESS_PREMIUM_SUBSCRIPTIONS, etc. + + -- Boosting & nitro + premium_boosters BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[], + premium_tier INTEGER NOT NULL DEFAULT 0, -- 0 = None, 1 = Tier 1, 2 = Tier 2, 3 = Tier 3 + premium_booster_count INTEGER NOT NULL DEFAULT 0, -- cached boost count + + -- Safety & moderation + widget_enabled BOOLEAN NOT NULL DEFAULT FALSE, + widget_channel_id BYTEA CHECK(length(widget_channel_id) = 26), + preferred_locale TEXT NOT NULL DEFAULT 'en-US', -- IETF BCP 47 language tag + + -- Audit & integrity + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + large BOOLEAN NOT NULL DEFAULT FALSE, -- >150 members + member_count INTEGER NOT NULL DEFAULT 0 -- cached member count +); + +-- Critical indexes +CREATE INDEX idx_guilds_owner_id ON guilds (owner_id); +CREATE INDEX idx_guilds_region ON guilds (region); +CREATE INDEX idx_guilds_verification_level ON guilds (verification_level); +CREATE INDEX idx_guilds_large ON guilds (large) WHERE large = TRUE; -- for pagination +CREATE INDEX idx_guilds_created_at ON guilds (created_at DESC); \ No newline at end of file diff --git a/bin-test/migrations/06_guild_members.sql b/bin-test/migrations/06_guild_members.sql new file mode 100644 index 0000000..d69e339 --- /dev/null +++ b/bin-test/migrations/06_guild_members.sql @@ -0,0 +1,36 @@ +CREATE TABLE guild_members ( + guild_id BYTEA NOT NULL CHECK(length(guild_id) = 26), + user_id BYTEA NOT NULL CHECK(length(user_id) = 26), + + -- Core identity + nick TEXT CHECK (LENGTH(COALESCE(nick, '')) <= 32), -- display name in guild (NULL = uses user global name) + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Roles & permissions + roles BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[], -- array of role IDs (26-byte each) + boosting_since TIMESTAMPTZ, -- when they started boosting (NULL = not boosting) + + -- Voice state (lightweight caching) + voice_channel_id BYTEA CHECK(length(voice_channel_id) = 26), + deafened BOOLEAN NOT NULL DEFAULT FALSE, + muted BOOLEAN NOT NULL DEFAULT FALSE, + + -- Moderation & management + pending BOOLEAN NOT NULL DEFAULT FALSE, -- requires membership screening + timed_out_until TIMESTAMPTZ, -- NULL = not timed out + + -- Audit & integrity + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Composite primary key + PRIMARY KEY (guild_id, user_id) +); + +-- Critical indexes +CREATE INDEX idx_guild_members_user_id ON guild_members (user_id); +CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id); +CREATE INDEX idx_guild_members_roles ON guild_members USING GIN (roles); -- for role-based lookups +CREATE INDEX idx_guild_members_voice_channel_id ON guild_members (voice_channel_id) WHERE voice_channel_id IS NOT NULL; + +-- For quick "all members in guild" queries (covering index) +CREATE INDEX idx_guild_members_guild_id_nick ON guild_members (guild_id, nick); \ No newline at end of file diff --git a/bin-test/migrations/07_attachments.sql b/bin-test/migrations/07_attachments.sql new file mode 100644 index 0000000..9629cf1 --- /dev/null +++ b/bin-test/migrations/07_attachments.sql @@ -0,0 +1,9 @@ +CREATE TABLE attachments ( + id BYTEA NOT NULL CHECK(length(id) = 26), + uploader_id BYTEA NOT NULL CHECK(length(uploader_id) = 26), + channel_id BYTEA CHECK(length(channel_id) = 26), + file_size INT NOT NULL, + cdn_url TEXT NOT NULL, + + PRIMARY KEY(id) +) \ No newline at end of file diff --git a/bin-test/migrations/101_user-foreign.sql b/bin-test/migrations/101_user-foreign.sql new file mode 100644 index 0000000..e69de29 diff --git a/bin-test/migrations/102_messages-foreign.sql b/bin-test/migrations/102_messages-foreign.sql new file mode 100644 index 0000000..4e52356 --- /dev/null +++ b/bin-test/migrations/102_messages-foreign.sql @@ -0,0 +1,5 @@ +ALTER TABLE messages + ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, + ADD FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (reply_message_id) REFERENCES messages(id) ON DELETE SET NULL \ No newline at end of file diff --git a/bin-test/migrations/103_nonce-foreign.sql b/bin-test/migrations/103_nonce-foreign.sql new file mode 100644 index 0000000..e69de29 diff --git a/bin-test/migrations/104_channels-foreign.sql b/bin-test/migrations/104_channels-foreign.sql new file mode 100644 index 0000000..d01fd16 --- /dev/null +++ b/bin-test/migrations/104_channels-foreign.sql @@ -0,0 +1,4 @@ +ALTER TABLE channels + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (parent_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (thread_owner_id) REFERENCES users(id) ON DELETE SET NULL diff --git a/bin-test/migrations/105_guilds-foreign.sql b/bin-test/migrations/105_guilds-foreign.sql new file mode 100644 index 0000000..5072a4d --- /dev/null +++ b/bin-test/migrations/105_guilds-foreign.sql @@ -0,0 +1,32 @@ +ALTER TABLE guilds + ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (afk_channel_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (system_channel_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (widget_channel_id) REFERENCES channels(id) ON DELETE SET NULL; + +-- For "my guilds" query +CREATE INDEX idx_guild_members_guild_id_user_id ON guild_members (guild_id, user_id); + +-- Ensure member_count is updated on membership changes +CREATE OR REPLACE FUNCTION update_guild_member_counts() + RETURNS TRIGGER AS $$ +DECLARE + new_count INTEGER; +BEGIN + IF TG_OP = 'INSERT' THEN + new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = NEW.guild_id); + UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = NEW.guild_id; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = OLD.guild_id); + UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = OLD.guild_id; + RETURN OLD; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_guild_counts_on_member_change + AFTER INSERT OR DELETE ON guild_members + FOR EACH ROW +EXECUTE FUNCTION update_guild_member_counts(); \ No newline at end of file diff --git a/bin-test/migrations/106_guild_members-foreign.sql b/bin-test/migrations/106_guild_members-foreign.sql new file mode 100644 index 0000000..90bd257 --- /dev/null +++ b/bin-test/migrations/106_guild_members-foreign.sql @@ -0,0 +1,4 @@ +ALTER TABLE guild_members + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (voice_channel_id) REFERENCES channels(id) ON DELETE SET NULL diff --git a/bin-test/migrations/107_attachments-foreign.sql b/bin-test/migrations/107_attachments-foreign.sql new file mode 100644 index 0000000..1b75b8c --- /dev/null +++ b/bin-test/migrations/107_attachments-foreign.sql @@ -0,0 +1,3 @@ +ALTER TABLE attachments + ADD FOREIGN KEY (uploader_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE \ No newline at end of file diff --git a/bin-test/src/main.rs b/bin-test/src/main.rs new file mode 100644 index 0000000..54b309c --- /dev/null +++ b/bin-test/src/main.rs @@ -0,0 +1,116 @@ +use cove_net_server::message::middleware::auth::AuthTokenMiddleware; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::{Arc}; +use std::time::Duration; +use sqlx::{Execute, Executor}; +use sqlx::postgres::PgQueryResult; +use sqlx::types::time::OffsetDateTime; +use cove_db::{CoveDB, CoveDBImpl}; +use cove_db::part::{BindQueryBuilder, SqlPart}; +use cove_db::part::condition::ConditionType; +use cove_db::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; +use cove_db::rows::user::{PartialUserRow, UserRow}; +use cove_db::types::user_status::UserStatus; +use cove_net_common::id::message_type::MessageType; +use cove_net_common::id::SnowflakeID; +use cove_net_server::{register_routes, CoveServer, DatabaseMiddleware, RootHandler}; +use cove_net_server::message::handlers::account::login::LoginMessageHandler; +use cove_net_server::message::handlers::account::register::RegisterMessageHandler; +use cove_net_server::message::handlers::Handler; +use cove_net_server::message::handlers::text::attachment::AttachmentMessageHandler; +use cove_net_server::message::handlers::text::reaction::ReactionMessageHandler; +use cove_net_server::message::handlers::text::text::TextMessageHandler; + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let db = CoveDB::new("localhost", 5432, "postgres", "password", "testdata").await?; + db.run_migrations().await?; + db.run_system_migrations().await?; + + // let user_row = UserRow { + // id: SnowflakeID::new_random_hex_loc(MessageType::User, "beefcafe")?, + // username: "CanadianBacon".to_string(), + // discriminator: "0001".to_string(), + // avatar_hash: None, + // email: "bc.bacon.bits@gmail.com".to_string(), + // email_verified: true, + // password_hash: "1802vgu12890n7b489127".to_string(), + // mfa_enabled: false, + // mfa_secret: None, + // status: UserStatus::Online, + // public_flags: 0, + // locale: "en-US".to_string(), + // premium_since: Some(OffsetDateTime::now_utc() + Duration::from_hours(1)), + // premium_end: Some(OffsetDateTime::now_utc() + Duration::from_hours(24)), + // bot: false, + // bot_oauth_scopes: Default::default(), + // preferences: Default::default(), + // created_at: OffsetDateTime::now_utc(), + // updated_at: OffsetDateTime::now_utc(), + // }; + // + // let mut query_builder = BindQueryBuilder::new(); + // user_row.insert().encode(&mut query_builder)?; + // + // let query = query_builder.to_query(); + // let query = user_row.bind(query)?; + // + // let res = db.run_query::(query.sql_query).await?; + // println!("{} rows affected", res.rows_affected()); + + let request_row: ::PartialRow = PartialUserRow { + username: Some("CanadianBacon".to_string()), + ..Default::default() + }; + + let mut query_builder = BindQueryBuilder::new(); + request_row.select(vec!["id", "username"]).encode(&mut query_builder)?; + let sql_where = request_row.wheres(|w|{ + w.cond_and::("username", ConditionType::Equal(false)) + })?; + sql_where.encode(&mut query_builder)?; + + let query = request_row.bind(sql_where, query_builder.to_query())?; + + println!("{}", query.sql_query.sql()); + let out_partial_row: ::PartialRow = db.get_pool().fetch_one(query.sql_query).await?.into(); + + println!("{:?}, {:?}, {:?}", out_partial_row.id, out_partial_row.username, out_partial_row.email); + + http_testing(db).await?; + + Ok(()) +} + +async fn http_testing(db: CoveDB) -> Result<(), anyhow::Error> { + let mut root_handler = Handler::new(Box::new(RootHandler)); + root_handler.add_middleware(AuthTokenMiddleware).await?; + root_handler.add_middleware(DatabaseMiddleware::new(Arc::new(db))).await?; + + register_routes!(&mut root_handler, + "text" => TextMessageHandler => { + "attachment" => AttachmentMessageHandler, + "reaction" => ReactionMessageHandler + }, + "login" => LoginMessageHandler, + "register" => RegisterMessageHandler + ).await?; + + let cove_server = match CoveServer::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + 8088, Arc::new(root_handler) + ).await { + Ok(server) => {server} + Err(err) => {panic!("Failed to construct CoveServer {}", err);} + }; + + match cove_server.run().await { + Ok(_) => { + println!("Cove server exited successfully!"); + Ok(()) + } + Err(err) => { + Err(err) + } + } +} \ No newline at end of file diff --git a/bin-test/system-migrations/201_nonce-management.sql b/bin-test/system-migrations/201_nonce-management.sql new file mode 100644 index 0000000..a3be75f --- /dev/null +++ b/bin-test/system-migrations/201_nonce-management.sql @@ -0,0 +1,21 @@ +INSERT INTO pgagent.pga_jobclass (jclname) + VALUES ('Cleanup Nonces'); + +INSERT INTO pgagent.pga_job (jobjclid, jobname, jobdesc, jobenabled, jobhostagent) +SELECT jcl.jclid, 'Cleanup Pending Nonces', 'Remove expired pending_nonces rows older than 5 minutes', true, '' +from pgagent.pga_jobclass jcl WHERE jclname='Cleanup Nonces'; + +INSERT INTO pgagent.pga_jobstep (jstjobid, jstname, jstdesc, jstenabled, jstkind, jstonerror, jstcode, jstdbname) +SELECT job.jobid, 'Perform Cleanup', 'Delete pending nonces', true, 's', 'f', $$DELETE FROM pending_nonces WHERE expires_at < NOW()$$, 'testdata' +FROM pgagent.pga_job job where jobname='Cleanup Pending Nonces'; + +INSERT INTO pgagent.pga_schedule (jscjobid, jscname, jscenabled, jscstart, jscminutes) +VALUES ( + (SELECT jobid + from pgagent.pga_job + where jobname = 'Cleanup Pending Nonces'), + 'Every 5 minutes', + true, + CURRENT_TIMESTAMP, -- start anytime + '{t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f}' -- every 5th minute + ); \ No newline at end of file diff --git a/cove-db/.env b/cove-db/.env new file mode 100644 index 0000000..a5d88b9 --- /dev/null +++ b/cove-db/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:password@localhost:5432/" \ No newline at end of file diff --git a/cove-db/Cargo.toml b/cove-db/Cargo.toml new file mode 100644 index 0000000..5415769 --- /dev/null +++ b/cove-db/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "cove-db" +version = "0.1.0" +edition = "2024" + +[dependencies] +sqlx.workspace = true +anyhow.workspace = true +serde_json.workspace = true + +cove-net-common.workspace = true \ No newline at end of file diff --git a/cove-db/migrations/01_user.sql b/cove-db/migrations/01_user.sql new file mode 100644 index 0000000..fdb3e8b --- /dev/null +++ b/cove-db/migrations/01_user.sql @@ -0,0 +1,30 @@ +-- user status enum values +DO $$ BEGIN + CREATE TYPE user_status AS ENUM ('online', 'idle', 'dnd', 'offline', 'invisible'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + + +-- main users table +CREATE TABLE users ( + id BYTEA PRIMARY KEY NOT NULL CHECK(length(id) = 26), + username TEXT NOT NULL CHECK (char_length(username) BETWEEN 1 AND 32), + discriminator TEXT NOT NULL DEFAULT '0000' CHECK (discriminator ~ '^\d{4}$'), + avatar_hash BYTEA CHECK(length(id) = 26), -- null if no avatar + email TEXT UNIQUE CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'), + email_verified BOOLEAN NOT NULL DEFAULT FALSE, + password_hash TEXT NOT NULL, -- bcrypt/argon2 hash, never plaintext + mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE, + mfa_secret TEXT CHECK (mfa_enabled = FALSE OR char_length(mfa_secret) > 0), -- encrypted if present + status user_status NOT NULL DEFAULT 'offline', + public_flags BIGINT NOT NULL DEFAULT 0, -- bitmask for flags + locale TEXT NOT NULL DEFAULT 'en-US' CHECK (locale ~ '^[a-z]{2}(-[A-Z]{2})?$'), -- BCP 47 pattern + premium_since TIMESTAMPTZ, -- e.g., Premium subscription start + premium_end TIMESTAMPTZ, + bot BOOLEAN NOT NULL DEFAULT FALSE, + bot_oauth_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- e.g., ['bot', 'guilds.join'] + preferences JSONB NOT NULL DEFAULT '{}'::jsonb, -- theme, sound, etc. + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/cove-db/migrations/02_messages.sql b/cove-db/migrations/02_messages.sql new file mode 100644 index 0000000..74e1269 --- /dev/null +++ b/cove-db/migrations/02_messages.sql @@ -0,0 +1,43 @@ +CREATE TABLE messages ( + id BYTEA NOT NULL CHECK(length(id) = 26), + channel_id BYTEA NOT NULL CHECK(length(channel_id) = 26), + guild_id BYTEA CHECK(length(guild_id) = 26), -- nullable for DMs + author_id BYTEA NOT NULL CHECK(length(author_id) = 26), + + -- Message content and metadata + content TEXT NOT NULL DEFAULT '', + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + edited_timestamp TIMESTAMPTZ, + tts BOOLEAN NOT NULL DEFAULT FALSE, + + -- Mentions and embeds (as JSON for flexibility) + mentions BYTEA[], -- array of user IDs (could also be TEXT[] if using base58) + mention_everyone BOOLEAN NOT NULL DEFAULT FALSE, + embeds JSONB NOT NULL DEFAULT '[]'::jsonb, + attachments BYTEA[], + + -- Reply/reference data (optional) + reply_message_id BYTEA CHECK(length(reply_message_id) = 26), + application_id BYTEA CHECK(length(application_id) = 26), -- for slash commands + + -- System/interaction message type + message_type INTEGER NOT NULL DEFAULT 0, -- 0 = default, 1 = reply, 2 = gateway ping, etc. + + -- Thread support + thread_name TEXT, + auto_archive_duration INTEGER, -- minutes (360, 1440, 4320, 10080) + + -- Audit & integrity + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + PRIMARY KEY (id) +); + +-- Indexes for common query patterns +CREATE INDEX idx_messages_channel_id ON messages (channel_id); +CREATE INDEX idx_messages_channel_id_timestamp ON messages (channel_id, timestamp DESC); +CREATE INDEX idx_messages_author_id ON messages (author_id); +CREATE INDEX idx_messages_guild_id ON messages (guild_id) WHERE guild_id IS NOT NULL; +CREATE INDEX idx_messages_reference_message_id ON messages (reply_message_id); +CREATE INDEX idx_messages_timestamp ON messages (timestamp DESC); diff --git a/cove-db/migrations/03_nonce.sql b/cove-db/migrations/03_nonce.sql new file mode 100644 index 0000000..c018242 --- /dev/null +++ b/cove-db/migrations/03_nonce.sql @@ -0,0 +1,24 @@ +-- Staging table for pending nonces (with TTL) +CREATE TABLE pending_nonces ( + nonce TEXT NOT NULL PRIMARY KEY, + channel_id BYTEA NOT NULL CHECK (LENGTH(channel_id) = 26), + author_id BYTEA NOT NULL CHECK (LENGTH(author_id) = 26), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL +); + +-- Index for expiry queries +CREATE INDEX idx_pending_nonces_expires_at ON pending_nonces (expires_at); + +CREATE OR REPLACE FUNCTION set_nonce_expiry() + RETURNS TRIGGER AS $$ +BEGIN + NEW.expires_at := NOW() + INTERVAL '5 minutes'; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trig_pending_nonces_expiry + BEFORE INSERT ON pending_nonces + FOR EACH ROW +EXECUTE FUNCTION set_nonce_expiry(); \ No newline at end of file diff --git a/cove-db/migrations/04_channels.sql b/cove-db/migrations/04_channels.sql new file mode 100644 index 0000000..a570b7c --- /dev/null +++ b/cove-db/migrations/04_channels.sql @@ -0,0 +1,45 @@ +CREATE TABLE channels ( + id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26), + guild_id BYTEA CHECK(length(guild_id) = 26), -- NULL for DMs & group DMs + parent_id BYTEA CHECK(length(parent_id) = 26), -- for thread categories / channel groups + + -- Core identity & type + name TEXT NOT NULL CHECK (LENGTH(name) >= 1 AND LENGTH(name) <= 100), + channel_type INTEGER NOT NULL DEFAULT 0, -- 0 = text, 1 = voice, 2 = category, 5 = news, etc. + + -- Permissions & visibility + position INTEGER NOT NULL DEFAULT 0, + permission_overwrites JSONB NOT NULL DEFAULT '[]'::jsonb, -- array of overwrites + rate_limit_per_user INTEGER NOT NULL DEFAULT 0, -- slowmode in seconds (0–21600) + + -- NSFW & visibility flags + nsfw BOOLEAN NOT NULL DEFAULT FALSE, + loud BOOLEAN NOT NULL DEFAULT FALSE, -- voice channel: triggers notifications + + -- Thread-specific fields (for threads spawned from messages) + thread_metadata JSONB, -- { "archived": bool, "auto_archive_duration": int, "archive_timestamp": timestamptz, "locked": bool } + member_count INTEGER NOT NULL DEFAULT 0, -- approximate member count (not real-time) + message_count INTEGER NOT NULL DEFAULT 0, -- cached message count (for UI previews) + thread_owner_id BYTEA CHECK(length(thread_owner_id) = 26), + + -- System metadata + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Topic (for news, text channels) + topic TEXT CHECK (LENGTH(COALESCE(topic, '')) <= 1024), + + -- Voice-specific + user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = no limit + region TEXT -- voice region (e.g., "us-west"), NULL = automatic +); + +-- Critical indexes for common operations +CREATE INDEX idx_channels_guild_id ON channels (guild_id) WHERE guild_id IS NOT NULL; +CREATE INDEX idx_channels_guild_id_position ON channels (guild_id, position); +CREATE INDEX idx_channels_parent_id ON channels (parent_id) WHERE parent_id IS NOT NULL; +CREATE INDEX idx_channels_channel_type ON channels (channel_type); +CREATE UNIQUE INDEX uidx_channels_channel_pos ON channels (guild_id, position); + +-- Optional: cover index for message listing (join + sort) +CREATE INDEX idx_channels_guild_id_created_at ON channels (guild_id, created_at) WHERE guild_id IS NOT NULL; \ No newline at end of file diff --git a/cove-db/migrations/05_guilds.sql b/cove-db/migrations/05_guilds.sql new file mode 100644 index 0000000..b0323b3 --- /dev/null +++ b/cove-db/migrations/05_guilds.sql @@ -0,0 +1,56 @@ +CREATE TABLE guilds ( + id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26), + + -- Core identity + name TEXT NOT NULL CHECK (LENGTH(name) >= 2 AND LENGTH(name) <= 100), + description TEXT CHECK (LENGTH(COALESCE(description, '')) <= 1024), + icon BYTEA CHECK(length(icon) = 26), + banner BYTEA CHECK(length(banner) = 26), + splash BYTEA CHECK(length(splash) = 26), + + -- Ownership & verification + owner_id BYTEA NOT NULL CHECK(length(owner_id) = 26), + owner_permissions BYTEA NOT NULL DEFAULT 'xFFFFFFFFFFFFFFFF'::BYTEA, -- 8-byte bitmask (e.g., ADMINISTRATOR = 0x8) + + -- Regions & voice + region TEXT NOT NULL DEFAULT 'us-west', -- voice region (e.g., 'us-west', 'eu-central') + + -- Features (bitmask of enabled features) + features INTEGER NOT NULL DEFAULT 0, -- 0 = basic, 1 = ANIMATED_ICON, 2 = BANNER, 4 = COMMERCE, 8 = PUBLIC, etc. + + -- Discovery & visibility + afk_channel_id BYTEA CHECK(length(afk_channel_id) = 26), + afk_timeout INTEGER NOT NULL DEFAULT 300, -- seconds (60, 300, 900, 1800, 3600) + verification_level INTEGER NOT NULL DEFAULT 0, -- 0 = none, 1 = low, 2 = medium, 3 = high, 4 = highest + default_message_notifications INTEGER NOT NULL DEFAULT 1, -- 0 = all, 1 = mentions only + + -- Explicit content filter + explicit_content_filter INTEGER NOT NULL DEFAULT 0, -- 0 = disabled, 1 = members without role, 2 = all + + -- System channels + system_channel_id BYTEA CHECK(length(system_channel_id) = 26), + system_channel_flags INTEGER NOT NULL DEFAULT 0, -- 1 = SUPPRESS_JOIN_NOTIFICATIONS, 2 = SUPPRESS_PREMIUM_SUBSCRIPTIONS, etc. + + -- Boosting & nitro + premium_boosters INTEGER NOT NULL DEFAULT 0, + premium_tier INTEGER NOT NULL DEFAULT 0, -- 0 = None, 1 = Tier 1, 2 = Tier 2, 3 = Tier 3 + premium_subscription_count INTEGER NOT NULL DEFAULT 0, -- cached boost count + + -- Safety & moderation + widget_enabled BOOLEAN NOT NULL DEFAULT FALSE, + widget_channel_id BYTEA CHECK(length(widget_channel_id) = 26), + preferred_locale TEXT NOT NULL DEFAULT 'en-US', -- IETF BCP 47 language tag + + -- Audit & integrity + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + joined_at TIMESTAMPTZ, -- when bot joined (if applicable) + large BOOLEAN NOT NULL DEFAULT FALSE, -- >150 members + member_count INTEGER NOT NULL DEFAULT 0 -- cached member count +); + +-- Critical indexes +CREATE INDEX idx_guilds_owner_id ON guilds (owner_id); +CREATE INDEX idx_guilds_region ON guilds (region); +CREATE INDEX idx_guilds_verification_level ON guilds (verification_level); +CREATE INDEX idx_guilds_large ON guilds (large) WHERE large = TRUE; -- for pagination +CREATE INDEX idx_guilds_created_at ON guilds (created_at DESC); \ No newline at end of file diff --git a/cove-db/migrations/06_guild_members.sql b/cove-db/migrations/06_guild_members.sql new file mode 100644 index 0000000..d69e339 --- /dev/null +++ b/cove-db/migrations/06_guild_members.sql @@ -0,0 +1,36 @@ +CREATE TABLE guild_members ( + guild_id BYTEA NOT NULL CHECK(length(guild_id) = 26), + user_id BYTEA NOT NULL CHECK(length(user_id) = 26), + + -- Core identity + nick TEXT CHECK (LENGTH(COALESCE(nick, '')) <= 32), -- display name in guild (NULL = uses user global name) + joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Roles & permissions + roles BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[], -- array of role IDs (26-byte each) + boosting_since TIMESTAMPTZ, -- when they started boosting (NULL = not boosting) + + -- Voice state (lightweight caching) + voice_channel_id BYTEA CHECK(length(voice_channel_id) = 26), + deafened BOOLEAN NOT NULL DEFAULT FALSE, + muted BOOLEAN NOT NULL DEFAULT FALSE, + + -- Moderation & management + pending BOOLEAN NOT NULL DEFAULT FALSE, -- requires membership screening + timed_out_until TIMESTAMPTZ, -- NULL = not timed out + + -- Audit & integrity + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Composite primary key + PRIMARY KEY (guild_id, user_id) +); + +-- Critical indexes +CREATE INDEX idx_guild_members_user_id ON guild_members (user_id); +CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id); +CREATE INDEX idx_guild_members_roles ON guild_members USING GIN (roles); -- for role-based lookups +CREATE INDEX idx_guild_members_voice_channel_id ON guild_members (voice_channel_id) WHERE voice_channel_id IS NOT NULL; + +-- For quick "all members in guild" queries (covering index) +CREATE INDEX idx_guild_members_guild_id_nick ON guild_members (guild_id, nick); \ No newline at end of file diff --git a/cove-db/migrations/101_user-foreign.sql b/cove-db/migrations/101_user-foreign.sql new file mode 100644 index 0000000..e69de29 diff --git a/cove-db/migrations/102_messages-foreign.sql b/cove-db/migrations/102_messages-foreign.sql new file mode 100644 index 0000000..4e52356 --- /dev/null +++ b/cove-db/migrations/102_messages-foreign.sql @@ -0,0 +1,5 @@ +ALTER TABLE messages + ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, + ADD FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (reply_message_id) REFERENCES messages(id) ON DELETE SET NULL \ No newline at end of file diff --git a/cove-db/migrations/103_nonce-foreign.sql b/cove-db/migrations/103_nonce-foreign.sql new file mode 100644 index 0000000..e69de29 diff --git a/cove-db/migrations/104_channels-foreign.sql b/cove-db/migrations/104_channels-foreign.sql new file mode 100644 index 0000000..d01fd16 --- /dev/null +++ b/cove-db/migrations/104_channels-foreign.sql @@ -0,0 +1,4 @@ +ALTER TABLE channels + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (parent_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (thread_owner_id) REFERENCES users(id) ON DELETE SET NULL diff --git a/cove-db/migrations/105_guilds-foreign.sql b/cove-db/migrations/105_guilds-foreign.sql new file mode 100644 index 0000000..5072a4d --- /dev/null +++ b/cove-db/migrations/105_guilds-foreign.sql @@ -0,0 +1,32 @@ +ALTER TABLE guilds + ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (afk_channel_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (system_channel_id) REFERENCES channels(id) ON DELETE SET NULL, + ADD FOREIGN KEY (widget_channel_id) REFERENCES channels(id) ON DELETE SET NULL; + +-- For "my guilds" query +CREATE INDEX idx_guild_members_guild_id_user_id ON guild_members (guild_id, user_id); + +-- Ensure member_count is updated on membership changes +CREATE OR REPLACE FUNCTION update_guild_member_counts() + RETURNS TRIGGER AS $$ +DECLARE + new_count INTEGER; +BEGIN + IF TG_OP = 'INSERT' THEN + new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = NEW.guild_id); + UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = NEW.guild_id; + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = OLD.guild_id); + UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = OLD.guild_id; + RETURN OLD; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_guild_counts_on_member_change + AFTER INSERT OR DELETE ON guild_members + FOR EACH ROW +EXECUTE FUNCTION update_guild_member_counts(); \ No newline at end of file diff --git a/cove-db/migrations/106_guild_members-foreign.sql b/cove-db/migrations/106_guild_members-foreign.sql new file mode 100644 index 0000000..90bd257 --- /dev/null +++ b/cove-db/migrations/106_guild_members-foreign.sql @@ -0,0 +1,4 @@ +ALTER TABLE guild_members + ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, + ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + ADD FOREIGN KEY (voice_channel_id) REFERENCES channels(id) ON DELETE SET NULL diff --git a/cove-db/src/lib.rs b/cove-db/src/lib.rs new file mode 100644 index 0000000..ef85b42 --- /dev/null +++ b/cove-db/src/lib.rs @@ -0,0 +1,78 @@ +pub mod query; +pub mod rows; +pub mod part; +pub mod types; + +use anyhow::Context; +use sqlx::{PgConnection, PgPool, Postgres}; +use sqlx::migrate::Migrator; +use sqlx::postgres::{PgArguments, PgConnectOptions, PgPoolOptions, PgQueryResult}; +use sqlx::query::Query; + +pub struct CoveDB { + pg_connect_options: PgConnectOptions, + db_pool: PgPool, +} + +impl CoveDB { + pub async fn new(host: &str, port: u16, username: &str, password: &str, database: &str) -> Result { + let pg_connect_options = ::Options::new() + .host(host) + .port(port) + .username(username) + .password(password) + .database(database); + + let db_pool = CoveDB::create_pool(10, pg_connect_options.clone()).await?; + + + + let db = CoveDB { + pg_connect_options, + db_pool + }; + + Ok(db) + } + + async fn create_pool(max_connections: u32, options: PgConnectOptions) -> Result { + PgPoolOptions::new() + .max_connections(max_connections) + .connect_with(options) + .await + .context("Failed to create database connection") + } + + pub async fn run_migrations(&self) -> Result<(), anyhow::Error> { + let m = Migrator::new(std::path::Path::new("./migrations")).await?; + m.run(&self.db_pool).await?; + + Ok(()) + } + + pub async fn run_system_migrations(&self) -> Result<(), anyhow::Error> { + let m = Migrator::new(std::path::Path::new("./system-migrations")).await?; + let db_pool = CoveDB::create_pool(5, self.pg_connect_options.clone().database("postgres")).await?; + m.run(&db_pool).await?; + + Ok(()) + } +} + +impl CoveDBImpl for CoveDB { + fn get_pool(&self) -> &PgPool { + &self.db_pool + } +} + +pub trait CoveDBImpl { + fn get_pool(&self) -> &PgPool; + + async fn run_query>(&self, query: Query<'_, Postgres, PgArguments>) -> Result { + let result = query.execute(self.get_pool()) + .await + .context("Failed to execute query")?; + + Ok(result.into()) + } +} \ No newline at end of file diff --git a/cove-db/src/part/condition.rs b/cove-db/src/part/condition.rs new file mode 100644 index 0000000..4b780ea --- /dev/null +++ b/cove-db/src/part/condition.rs @@ -0,0 +1,83 @@ +use std::marker::PhantomData; +use sqlx::{Encode, Postgres, Type}; +use crate::part::{BindQueryBuilder, SqlPart}; + +#[derive(Clone)] +pub enum ConditionType { + GreaterThan, + GreaterOrEqual, + LessThan, + LessOrEqual, + Equal(bool), + In(bool), + Between(bool), + Null(bool), + Like(bool), + Exists(bool), +} + +#[derive(Clone)] +pub enum ConditionJoin { + And, + Or +} + +impl Into<&str> for ConditionType { + fn into(self) -> &'static str { + match self { + ConditionType::GreaterThan => {" > "} + ConditionType::GreaterOrEqual => {" >= "} + ConditionType::LessThan => {" < "} + ConditionType::LessOrEqual => {" <= "} + ConditionType::Equal(not) => { + if not { " != " } else { " = " } + } + ConditionType::In(not) => { + if not { " NOT IN " } else { " IN " } + } + ConditionType::Between(not) => { + if not { " NOT BETWEEN " } else { " BETWEEN " } + } + ConditionType::Null(not) => { + if not { " NOT NULL " } else { " NULL " } + } + ConditionType::Like(not) => { + if not { " NOT LIKE " } else { " LIKE " } + } + ConditionType::Exists(not) => { + if not { " NOT EXISTS " } else { " EXISTS " } + } + } + } +} + +impl Into<&str> for ConditionJoin { + fn into(self) -> &'static str { + match self { + ConditionJoin::And => " AND ", + ConditionJoin::Or => " OR " + } + } +} + +pub struct SqlCondition + Type + Clone + 'static> { + pub key: String, + condition_type: ConditionType, + value_type: PhantomData, + pub condition_join: ConditionJoin, +} + +impl + Type + Clone> SqlCondition { + pub fn new(key: impl Into, condition_type: ConditionType, condition_join: ConditionJoin) -> SqlCondition { + SqlCondition { key: key.into(), condition_type, value_type: PhantomData::, condition_join } + } +} + +impl + Type + Clone> SqlPart for SqlCondition { + fn encode(&self, query_builder: &mut BindQueryBuilder) -> Result<(), anyhow::Error> { + query_builder.push(&self.key); + query_builder.push(>::into(self.condition_type.clone())); + query_builder.create_bind::(); + Ok(()) + } +} \ No newline at end of file diff --git a/cove-db/src/part/insert.rs b/cove-db/src/part/insert.rs new file mode 100644 index 0000000..a40444e --- /dev/null +++ b/cove-db/src/part/insert.rs @@ -0,0 +1,75 @@ +use std::any::TypeId; +use anyhow::Error; +use sqlx::{Encode, Postgres, Type}; +use crate::part::{BindQueryBuilder, SqlPart}; + +pub struct SqlInsert { + table_name: String, + columns: Vec<(String, TypeId)> +} + +impl SqlInsert { + pub fn with_table(table_name: impl Into) -> Self { + Self {table_name: table_name.into(), columns: Vec::new()} + } + + pub fn col + Type + Clone + 'static>(mut self, column_name: impl Into) -> Self { + self.columns.push((column_name.into(), TypeId::of::())); + self + } + + pub fn opt_col + Type + Clone + 'static>(mut self, column_name: impl Into) -> Self { + self.columns.push((column_name.into(), TypeId::of::>())); + self + } + + pub fn arr_col + Type + Clone + 'static>(mut self, column_name: impl Into) -> Self { + self.columns.push((column_name.into(), TypeId::of::>())); + self + } + + pub fn opt_arr_col + Type + Clone + 'static>(mut self, column_name: impl Into) -> Self { + self.columns.push((column_name.into(), TypeId::of::>>())); + self + } +} + +impl SqlPart for SqlInsert { + fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> { + arg_buffer.push("INSERT INTO ") + .push(&self.table_name) + .push("("); + + let mut cols = vec![]; + let mut types = vec![]; + for (column_name, column_type) in &self.columns { + cols.push(column_name.as_str()); + types.push(column_type.clone()); + } + + let end_idx = cols.len(); + let mut i = 0; + for col in cols { + arg_buffer.push(col); + if i < end_idx-1 { + arg_buffer.push(","); + } + i += 1; + } + + arg_buffer.push(") VALUES ("); + + i = 0; + for ty in types { + unsafe { + arg_buffer.create_bind_raw_unchecked(ty); + } + if i < end_idx-1 { + arg_buffer.push(","); + } + i += 1 + } + arg_buffer.push(")"); + Ok(()) + } +} \ No newline at end of file diff --git a/cove-db/src/part/mod.rs b/cove-db/src/part/mod.rs new file mode 100644 index 0000000..9f2f2b1 --- /dev/null +++ b/cove-db/src/part/mod.rs @@ -0,0 +1,95 @@ +use std::fmt::Write; +use std::any::TypeId; +use std::collections::HashMap; +use std::fmt::Display; +use sqlx::postgres::{PgArguments}; +use sqlx::{Encode, Postgres, Type}; +use sqlx::query::Query; + +pub mod condition; +pub mod select; +pub mod sql_where; +pub mod insert; + +pub trait SqlPart { + fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), anyhow::Error>; +} + + +#[derive(Default, Clone)] +pub struct BindQueryBuilder { + pub sql_string: String, + type_map: HashMap, + bind_index: i32, +} + +impl BindQueryBuilder { + pub fn new() -> Self { + BindQueryBuilder { + ..Default::default() + } + } + + pub fn push(&mut self, sql: impl Display) -> &mut Self { + self.sql_string.push_str(sql.to_string().as_str()); + self + } + + pub fn create_bind<'args, T: 'args + Encode<'args, Postgres> + Type + 'static>(&mut self) -> &mut Self { + self.bind_index += 1; + write!(self.sql_string, "${}", self.bind_index).expect("error formatting `sql`"); + self.type_map.insert(self.bind_index, TypeId::of::()); + self + } + + pub unsafe fn create_bind_raw_unchecked(&mut self, type_id: TypeId) -> &mut Self { + self.bind_index += 1; + write!(self.sql_string, "${}", self.bind_index).expect("error formatting `sql`"); + self.type_map.insert(self.bind_index, type_id); + self + } + + pub fn to_query(&'_ self) -> BindQuery<'_> { + BindQuery::new(self) + } +} + +pub struct BindQuery<'args> { + pub bind_query: &'args BindQueryBuilder, + pub sql_query: Query<'args, Postgres, PgArguments>, + index: i32 +} + +impl<'args> BindQuery<'args> { + fn new(bind_query: &'args BindQueryBuilder) -> Self { + BindQuery { + bind_query, + sql_query: sqlx::query(&bind_query.sql_string), + index: 1 + } + } + + pub fn bind + Type + 'static>(mut self, value: &'args T) -> Result { + if let Some(type_id) = self.bind_query.type_map.get(&self.index) { + if type_id == &TypeId::of::() { + match self.sql_query.try_bind(value) { + Err(e) => { + Err(anyhow::anyhow!("Failed to bind in PreparedBindQuery: {}", e)) + } + _ => { + self.index += 1; + Ok(self) + } + } + } else { + Err(anyhow::anyhow!("Type not suitable for parameter with index {}", self.index)) + } + } else { + Err(anyhow::anyhow!("Index {} is out of bounds for query.", self.index)) + } + } + + pub fn build(self) -> Query<'args, Postgres, PgArguments> { + self.sql_query + } +} \ No newline at end of file diff --git a/cove-db/src/part/select.rs b/cove-db/src/part/select.rs new file mode 100644 index 0000000..1fc527d --- /dev/null +++ b/cove-db/src/part/select.rs @@ -0,0 +1,39 @@ +use crate::part::{BindQueryBuilder, SqlPart}; + +pub struct SqlSelect { + selected_columns: Vec, + table_name: String +} + +impl SqlSelect { + pub fn with_table(table_name: impl Into) -> SqlSelect { + SqlSelect {selected_columns: vec![], table_name: table_name.into()} + } + + pub fn add_column(&mut self, column: impl Into) -> &mut Self { + self.selected_columns.push(column.into()); + self + } +} + +impl SqlPart for SqlSelect { + fn encode(&self, query_builder: &mut BindQueryBuilder) -> Result<(), anyhow::Error> { + query_builder.push("SELECT "); + + let mut i = 0; + let total = self.selected_columns.len(); + for column in &self.selected_columns { + query_builder.push(column); + if i < total - 1 { + query_builder.push(", "); + } + i += 1; + } + + query_builder.push(" FROM "); + query_builder.push(&self.table_name); + query_builder.push(" "); + + Ok(()) + } +} \ No newline at end of file diff --git a/cove-db/src/part/sql_where.rs b/cove-db/src/part/sql_where.rs new file mode 100644 index 0000000..ae93b6a --- /dev/null +++ b/cove-db/src/part/sql_where.rs @@ -0,0 +1,164 @@ +use std::sync::{Arc, Mutex}; +use anyhow::{anyhow, Error}; +use sqlx::{Encode, Postgres, Type}; +use crate::part::condition::{ConditionJoin, ConditionType, SqlCondition}; +use crate::part::{BindQueryBuilder, SqlPart}; + +pub struct SqlWhere { + selected_scope: Arc>, + pub indexed_placeholders: Vec<(usize, String)>, + index: usize +} + +impl SqlWhere { + pub fn new() -> Self { + let selected_scope = Arc::new(Mutex::new(SqlWhereScope::new(ConditionJoin::And))); + + SqlWhere { + selected_scope, + indexed_placeholders: Vec::new(), + index: 1 + } + } + + pub fn add_condition + Type + Clone + 'static>(mut self, condition: SqlCondition) -> Result { + match self.selected_scope.lock().map_err(|e|anyhow!("{}", e)) { + Ok(mut lock) => { + let idx = lock.get_index(); + let join = condition.condition_join.clone(); + self.indexed_placeholders.push((self.index, condition.key.clone())); + self.index += 1; + lock.conditions.push((idx, Box::new(move |ab|condition.encode(ab)), join)); + } + Err(e) => { + return Err(e); + } + } + Ok(self) + } + + pub fn scoped(mut self, condition_join: ConditionJoin, scope_fn: impl Fn(Self) -> Result) -> Result { + let mut scope = SqlWhereScope::new(condition_join); + scope.parent_scope = Some(self.selected_scope.clone()); + + let scope = Arc::new(Mutex::new(scope)); + if let Ok(mut selected_scope) = self.selected_scope.lock().map_err(|e|anyhow!("{}", e)) { + let idx = selected_scope.get_index(); + selected_scope.scopes.push((idx, scope.clone())); + } + + self.selected_scope = scope; + scope_fn(self)?.escape_scope() + } + + pub fn cond_and + Type + Clone + 'static>(self, key: impl Into, condition_type: ConditionType) -> Result { + self.add_condition(SqlCondition::::new(key, condition_type, ConditionJoin::And)) + } + + pub fn cond_or + Type + Clone + 'static>(self, key: impl Into, condition_type: ConditionType) -> Result { + self.add_condition(SqlCondition::::new(key, condition_type, ConditionJoin::Or)) + } + + pub fn cond_scope_and(self, scope_fn: impl Fn(Self) -> Result) -> Result { + self.scoped(ConditionJoin::And, scope_fn) + } + + pub fn cond_scope_or(self, scope_fn: impl Fn(Self) -> Result) -> Result { + self.scoped(ConditionJoin::Or, scope_fn) + } + + pub fn escape_scope(mut self) -> Result { + let parent_scope = if let Ok(scope) = + self.selected_scope.lock().map_err(|e|anyhow!("{}", e))? + .get_parent_scope() + { + scope.clone() + } else { + return Err(anyhow!("Already in root scope for SqlWhere")); + }; + + self.selected_scope = parent_scope; + Ok(self) + } +} + +impl SqlPart for SqlWhere { + fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> { + arg_buffer.push("WHERE "); + self.selected_scope.lock().map_err(|e|anyhow!("{}", e))?.encode(arg_buffer) + } +} + +pub struct SqlWhereScope { + conditions: Vec<(i32, Box Result<(), Error>>, ConditionJoin)>, + scopes: Vec<(i32, Arc>)>, + condition_join: ConditionJoin, + parent_scope: Option>>, + index: i32 +} + +impl SqlWhereScope { + pub fn new(condition_join: ConditionJoin) -> Self { + SqlWhereScope { + conditions: vec![], + scopes: vec![], + condition_join, + parent_scope: None, + index: 0 + } + } + + fn get_parent_scope(&mut self) -> Result>, Error> { + match self.parent_scope.clone() { + None => { + Err(anyhow!("Already in root scope for SqlWhere")) + } + Some(val) => { + Ok(val) + } + } + } + + fn get_index(&mut self) -> i32 { + self.index += 1; + self.index - 1 + } +} + +impl SqlPart for SqlWhereScope { + fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> { + let mut condition_found; + for i in 0..self.index { + condition_found = false; + for (j, condition, condition_join) in &self.conditions { + if i == *j { + condition_found = true; + condition(arg_buffer)?; + + if i < self.index-1 { + let join: &str = condition_join.clone().into(); + arg_buffer.push(join); + } + } + } + if condition_found { + continue; + } + for (j, scope) in &self.scopes { + if i == *j { + arg_buffer.push("("); + let lock = scope.lock().map_err(|e|anyhow!("{}", e))?; + lock.encode(arg_buffer)?; + arg_buffer.push(")"); + + if i < self.index-1 { + let join: &str = lock.condition_join.clone().into(); + arg_buffer.push(join); + } + } + } + } + + Ok(()) + } +} \ No newline at end of file diff --git a/cove-db/src/query/mod.rs b/cove-db/src/query/mod.rs new file mode 100644 index 0000000..71b75d5 --- /dev/null +++ b/cove-db/src/query/mod.rs @@ -0,0 +1 @@ +pub mod text; \ No newline at end of file diff --git a/cove-db/src/query/text.rs b/cove-db/src/query/text.rs new file mode 100644 index 0000000..e821bef --- /dev/null +++ b/cove-db/src/query/text.rs @@ -0,0 +1,59 @@ +use anyhow::{Error}; +use sqlx::{Execute, Executor}; +use sqlx::postgres::{ PgQueryResult}; +use cove_net_common::id::message_type::MessageType; +use cove_net_common::id::SnowflakeID; +use cove_net_common::id::types::text::TextMessageType; +use cove_net_common::message::c2s::text::text::{TextEmbed, TextMessage}; +use crate::{CoveDB, CoveDBImpl}; +use crate::part::{BindQueryBuilder, SqlPart}; +use crate::part::insert::SqlInsert; + +pub trait TextQueries: CoveDBImpl + Send + Sync { + async fn create_message( + &self, user_id: SnowflakeID, + message: TextMessage, mentions: Option>, mention_everyone: bool, + message_type: &TextMessageType + ) -> Result { + println!("creating message"); + let message_id = SnowflakeID::new_random_hex_loc( + MessageType::Text(TextMessageType::Text), "beefcafe" + )?; + + let mut query_builder = BindQueryBuilder::new(); + + SqlInsert::with_table("messages") + .col::("id") + .col::("channel_id") + .col::("guild_id") + .col::("author_id") + .col::("content") + .col::("tts") + .col::("mention_everyone") + .col::("message_type") + .opt_arr_col::("mentions") + .opt_arr_col::("embeds") + .opt_arr_col::("attachments") + .encode(&mut query_builder)?; + + let query = query_builder.to_query().build() + .bind(message_id) + .bind(message.channel_id) + .bind(message.guild_id) + .bind(user_id) + .bind(message.content) + .bind(message.tts) + .bind(mention_everyone) + .bind(message_type) + .bind(mentions) + .bind(message.embeds) + .bind(message.attachments); + + println!("{:?}", query.sql()); + let res: PgQueryResult = self.run_query(query).await?; + println!("Rows Affected: {}", res.rows_affected()); + Ok(res) + } +} + +impl TextQueries for CoveDB {} \ No newline at end of file diff --git a/cove-db/src/rows/attachment.rs b/cove-db/src/rows/attachment.rs new file mode 100644 index 0000000..13558a2 --- /dev/null +++ b/cove-db/src/rows/attachment.rs @@ -0,0 +1,77 @@ +use anyhow::anyhow; +use sqlx::postgres::PgRow; +use sqlx::Row; +use cove_net_common::id::SnowflakeID; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::select::SqlSelect; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +pub struct AttachmentRow { + pub id: SnowflakeID, + pub uploader_id: SnowflakeID, + pub channel_id: Option, + pub file_size: i32, + pub cdn_url: String +} + +impl TableRow for AttachmentRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "attachments" + } +} + +impl InsertableRow for AttachmentRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("id") + .col::("uploader_id") + .opt_col::("channel_id") + .col::("file_size") + .col::("cdn_url") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.id)? + .bind(&self.uploader_id)? + .bind(&self.channel_id)? + .bind(&self.file_size)? + .bind(&self.cdn_url) + } +} + +impl SelectableRow for AttachmentRow {} + +impl WhereRow for AttachmentRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "id" => query.bind(&self.id)?, + "uploader_id" => query.bind(&self.uploader_id)?, + "channel_id" => query.bind(&self.channel_id)?, + "file_size" => query.bind(&self.file_size)?, + "cdn_url" => query.bind(&self.cdn_url)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for AttachmentRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self { + id: value.try_get("id")?, + uploader_id: value.try_get("uploader_id")?, + channel_id: value.try_get("channel_id")?, + file_size: value.try_get("file_size")?, + cdn_url: value.try_get("cdn_url")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/channel.rs b/cove-db/src/rows/channel.rs new file mode 100644 index 0000000..ad14308 --- /dev/null +++ b/cove-db/src/rows/channel.rs @@ -0,0 +1,156 @@ +use anyhow::anyhow; +use serde_json::Value; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::id::SnowflakeID; +use cove_net_common::id::types::channel::ChannelMessageType; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +pub struct ChannelRow { + pub id: SnowflakeID, + pub guild_id: Option, + pub parent_id: Option, + + pub name: String, + pub channel_type: ChannelMessageType, + + pub position: i32, + pub permission_overwrites: Value, + pub rate_limit_per_user: i32, + + pub nsfw: bool, + pub loud: bool, + + pub thread_metadata: Option, + pub member_count: i32, + pub message_count: i32, + pub thread_owner_id: Option, + + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, + + pub topic: Option, + + pub user_limit: i32, + pub region: Option +} + +impl TableRow for ChannelRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "channels" + } +} + +impl InsertableRow for ChannelRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("id") + .opt_col::("guild_id") + .opt_col::("parent_id") + .col::("name") + .col::("channel_type") + .col::("position") + .col::("permission_overwrites") + .col::("rate_limit_per_user") + .col::("nsfw") + .col::("loud") + .opt_col::("thread_metadata") + .col::("member_count") + .col::("message_count") + .opt_col::("thread_owner_id") + .col::("created_at") + .col::("updated_at") + .opt_col::("topic") + .col::("user_limit") + .opt_col::("region") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.id)? + .bind(&self.guild_id)? + .bind(&self.parent_id)? + .bind(&self.name)? + .bind(&self.channel_type)? + .bind(&self.position)? + .bind(&self.permission_overwrites)? + .bind(&self.rate_limit_per_user)? + .bind(&self.nsfw)? + .bind(&self.loud)? + .bind(&self.thread_metadata)? + .bind(&self.member_count)? + .bind(&self.message_count)? + .bind(&self.thread_owner_id)? + .bind(&self.created_at)? + .bind(&self.updated_at)? + .bind(&self.topic)? + .bind(&self.user_limit)? + .bind(&self.region) + } +} + +impl SelectableRow for ChannelRow {} + +impl WhereRow for ChannelRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "id" => query.bind(&self.id)?, + "guild_id" => query.bind(&self.guild_id)?, + "parent_id" => query.bind(&self.parent_id)?, + "name" => query.bind(&self.name)?, + "channel_type" => query.bind(&self.channel_type)?, + "position" => query.bind(&self.position)?, + "permission_overwrites" => query.bind(&self.permission_overwrites)?, + "rate_limit_per_user" => query.bind(&self.rate_limit_per_user)?, + "nsfw" => query.bind(&self.nsfw)?, + "loud" => query.bind(&self.loud)?, + "thread_metadata" => query.bind(&self.thread_metadata)?, + "member_count" => query.bind(&self.member_count)?, + "message_count" => query.bind(&self.message_count)?, + "thread_owner_id" => query.bind(&self.thread_owner_id)?, + "created_at" => query.bind(&self.created_at)?, + "updated_at" => query.bind(&self.updated_at)?, + "topic" => query.bind(&self.topic)?, + "user_limit" => query.bind(&self.user_limit)?, + "region" => query.bind(&self.region)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for ChannelRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self{ + id: value.try_get("id")?, + guild_id: value.try_get("guild_id")?, + parent_id: value.try_get("parent_id")?, + name: value.try_get("name")?, + channel_type: value.try_get("channel_type")?, + position: value.try_get("position")?, + permission_overwrites: value.try_get("permission_overwrites")?, + rate_limit_per_user: value.try_get("rate_limit_per_user")?, + nsfw: value.try_get("nsfw")?, + loud: value.try_get("loud")?, + thread_metadata: value.try_get("thread_metadata")?, + member_count: value.try_get("member_count")?, + message_count: value.try_get("message_count")?, + thread_owner_id: value.try_get("thread_owner_id")?, + created_at: value.try_get("created_at")?, + updated_at: value.try_get("updated_at")?, + topic: value.try_get("topic")?, + user_limit: value.try_get("user_limit")?, + region: value.try_get("region")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/guild.rs b/cove-db/src/rows/guild.rs new file mode 100644 index 0000000..e147dcf --- /dev/null +++ b/cove-db/src/rows/guild.rs @@ -0,0 +1,200 @@ +use anyhow::anyhow; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::guild::component::default_notification::DefaultMessageNotificationSetting; +use cove_net_common::guild::component::explicit_filter::ExplicitContentFilterSetting; +use cove_net_common::guild::component::verification_level::VerificationLevelSetting; +use cove_net_common::id::SnowflakeID; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +struct GuildRow { + pub id: SnowflakeID, + + pub name: String, + pub description: Option, + pub icon: Option, + pub banner: Option, + pub splash: Option, + + pub owner_id: SnowflakeID, + pub owner_permissions: [u8; 8], + + pub region: String, + + pub features: i32, + + pub afk_channel_id: Option, + pub afk_timeout: i32, + pub verification_level: VerificationLevelSetting, + pub default_message_notifications: DefaultMessageNotificationSetting, + + pub explicit_content_filter: ExplicitContentFilterSetting, + + pub system_channel_id: Option, + pub system_channel_flags: i32, + + pub premium_boosters: Vec, + pub premium_tier: i32, + pub premium_booster_count: i32, + + pub widget_enabled: bool, + pub widget_channel_id: Option, + pub preferred_locale: String, + + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, + pub large: bool, + pub member_count: i32 +} + +impl TableRow for GuildRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "guilds" + } +} + +impl InsertableRow for GuildRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("id") + .col::("name") + .opt_col::("description") + .opt_col::("icon") + .opt_col::("banner") + .opt_col::("splash") + .col::("owner_id") + .opt_col::<[u8; 8]>("owner_permissions") + .col::("region") + .col::("features") + .opt_col::("afk_channel_id") + .col::("afk_timeout") + .col::("verification_level") + .col::("default_message_notifications") + .col::("explicit_content_filter") + .opt_col::("system_channel_id") + .opt_col::("system_channel_flags") + .arr_col::("premium_boosters") + .col::("premium_tier") + .col::("premium_booster_count") + .col::("widget_enabled") + .opt_col::("widget_channel_id") + .col::("preferred_locale") + .col::("created_at") + .col::("updated_at") + .col::("large") + .col::("member_count") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.id)? + .bind(&self.name)? + .bind(&self.description)? + .bind(&self.icon)? + .bind(&self.banner)? + .bind(&self.splash)? + .bind(&self.owner_id)? + .bind(&self.owner_permissions)? + .bind(&self.region)? + .bind(&self.features)? + .bind(&self.afk_channel_id)? + .bind(&self.afk_timeout)? + .bind(&self.verification_level)? + .bind(&self.default_message_notifications)? + .bind(&self.explicit_content_filter)? + .bind(&self.system_channel_id)? + .bind(&self.system_channel_flags)? + .bind(&self.premium_boosters)? + .bind(&self.premium_tier)? + .bind(&self.premium_booster_count)? + .bind(&self.widget_enabled)? + .bind(&self.widget_channel_id)? + .bind(&self.preferred_locale)? + .bind(&self.created_at)? + .bind(&self.updated_at)? + .bind(&self.large)? + .bind(&self.member_count) + } +} + +impl SelectableRow for GuildRow {} + +impl WhereRow for GuildRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "id" => query.bind(&self.id)?, + "name" => query.bind(&self.name)?, + "description" => query.bind(&self.description)?, + "icon" => query.bind(&self.icon)?, + "banner" => query.bind(&self.banner)?, + "splash" => query.bind(&self.splash)?, + "owner_id" => query.bind(&self.owner_id)?, + "owner_permissions" => query.bind(&self.owner_permissions)?, + "region" => query.bind(&self.region)?, + "features" => query.bind(&self.features)?, + "afk_channel_id" => query.bind(&self.afk_channel_id)?, + "afk_timeout" => query.bind(&self.afk_timeout)?, + "verification_level" => query.bind(&self.verification_level)?, + "default_message_notifications" => query.bind(&self.default_message_notifications)?, + "explicit_content_filter" => query.bind(&self.explicit_content_filter)?, + "system_channel_id" => query.bind(&self.system_channel_id)?, + "system_channel_flags" => query.bind(&self.system_channel_flags)?, + "premium_boosters" => query.bind(&self.premium_boosters)?, + "premium_tier" => query.bind(&self.premium_tier)?, + "premium_booster_count" => query.bind(&self.premium_booster_count)?, + "widget_enabled" => query.bind(&self.widget_enabled)?, + "widget_channel_id" => query.bind(&self.widget_channel_id)?, + "preferred_locale" => query.bind(&self.preferred_locale)?, + "created_at" => query.bind(&self.created_at)?, + "updated_at" => query.bind(&self.updated_at)?, + "large" => query.bind(&self.large)?, + "member_count" => query.bind(&self.member_count)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for GuildRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self { + id: value.try_get("id")?, + name: value.try_get("name")?, + description: value.try_get("description")?, + icon: value.try_get("icon")?, + banner: value.try_get("banner")?, + splash: value.try_get("splash")?, + owner_id: value.try_get("owner_id")?, + owner_permissions: value.try_get("owner_permissions")?, + region: value.try_get("region")?, + features: value.try_get("features")?, + afk_channel_id: value.try_get("afk_channel_id")?, + afk_timeout: value.try_get("afk_timeout")?, + verification_level: value.try_get("verification_level")?, + default_message_notifications: value.try_get("default_message_notifications")?, + explicit_content_filter: value.try_get("explicit_content_filter")?, + system_channel_id: value.try_get("system_channel_id")?, + system_channel_flags: value.try_get("system_channel_flags")?, + premium_boosters: value.try_get("premium_boosters")?, + premium_tier: value.try_get("premium_tier")?, + premium_booster_count: value.try_get("premium_booster_count")?, + widget_enabled: value.try_get("widget_enabled")?, + widget_channel_id: value.try_get("widget_channel_id")?, + preferred_locale: value.try_get("preferred_locale")?, + created_at: value.try_get("created_at")?, + updated_at: value.try_get("updated_at")?, + large: value.try_get("large")?, + member_count: value.try_get("member_count")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/guild_member.rs b/cove-db/src/rows/guild_member.rs new file mode 100644 index 0000000..c5d7333 --- /dev/null +++ b/cove-db/src/rows/guild_member.rs @@ -0,0 +1,117 @@ +use anyhow::anyhow; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::id::SnowflakeID; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +struct GuildMemberRow { + pub guild_id: SnowflakeID, + pub user_id: SnowflakeID, + + pub nick: Option, + pub joined_at: OffsetDateTime, + + pub roles: Vec, + pub boosting_since: Option, + + pub voice_channel_id: Option, + pub deafened: bool, + pub muted: bool, + + pub pending: bool, + pub timed_out_until: Option, + + pub updated_at: OffsetDateTime, +} + +impl TableRow for GuildMemberRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "guild_members" + } +} + +impl InsertableRow for GuildMemberRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("guild_id") + .col::("user_id") + .opt_col::("nick") + .col::("joined_at") + .arr_col::("roles") + .opt_col::("boosting_since") + .opt_col::("voice_channel_id") + .col::("deafened") + .col::("muted") + .col::("pending") + .opt_col::("timed_out_until") + .col::("updated_at") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.guild_id)? + .bind(&self.user_id)? + .bind(&self.nick)? + .bind(&self.joined_at)? + .bind(&self.roles)? + .bind(&self.boosting_since)? + .bind(&self.voice_channel_id)? + .bind(&self.deafened)? + .bind(&self.muted)? + .bind(&self.pending)? + .bind(&self.timed_out_until)? + .bind(&self.updated_at) + } +} + +impl SelectableRow for GuildMemberRow {} + +impl WhereRow for GuildMemberRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "guild_id" => query.bind(&self.guild_id)?, + "user_id" => query.bind(&self.user_id)?, + "nick" => query.bind(&self.nick)?, + "joined_at" => query.bind(&self.joined_at)?, + "roles" => query.bind(&self.roles)?, + "boosting_since" => query.bind(&self.boosting_since)?, + "voice_channel_id" => query.bind(&self.voice_channel_id)?, + "deafened" => query.bind(&self.deafened)?, + "muted" => query.bind(&self.muted)?, + "pending" => query.bind(&self.pending)?, + "timed_out_until" => query.bind(&self.timed_out_until)?, + "updated_at" => query.bind(&self.updated_at)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for GuildMemberRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self { + guild_id: value.try_get("guild_id")?, + user_id: value.try_get("user_id")?, + nick: value.try_get("nick")?, + joined_at: value.try_get("joined_at")?, + roles: value.try_get("roles")?, + boosting_since: value.try_get("boosting_since")?, + voice_channel_id: value.try_get("voice_channel_id")?, + deafened: value.try_get("deafened")?, + muted: value.try_get("muted")?, + pending: value.try_get("pending")?, + timed_out_until: value.try_get("timed_out_until")?, + updated_at: value.try_get("updated_at")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/message.rs b/cove-db/src/rows/message.rs new file mode 100644 index 0000000..a6be473 --- /dev/null +++ b/cove-db/src/rows/message.rs @@ -0,0 +1,155 @@ +use anyhow::anyhow; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::id::SnowflakeID; +use cove_net_common::id::types::text::TextMessageType; +use cove_net_common::message::c2s::text::text::TextEmbed; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +pub struct MessageRow { + pub id: SnowflakeID, + pub channel_id: SnowflakeID, + pub guild_id: Option, + pub author_id: SnowflakeID, + + pub content: String, + pub timestamp: OffsetDateTime, + pub edited_timestamp: Option, + pub tts: bool, + + pub mentions: Option>, + pub mention_everyone: bool, + pub embeds: Option>, + pub attachments: Option>, + + pub reply_message_id: Option, + pub application_id: Option, + + pub message_type: TextMessageType, + + pub thread_name: Option, + pub auto_archive_duration: Option, + + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +impl TableRow for MessageRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "messages" + } +} + +impl InsertableRow for MessageRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("id") + .col::("channel_id") + .opt_col::("guild_id") + .col::("author_id") + .col::("content") + .col::("timestamp") + .opt_col::("edited_timestamp") + .col::("tts") + .opt_arr_col::("mentions") + .col::("mention_everyone") + .opt_arr_col::("embeds") + .opt_arr_col::("attachments") + .opt_col::("reply_message_id") + .opt_col::("application_id") + .col::("message_type") + .opt_col::("thread_name") + .opt_col::("auto_archive_duration") + .col::("created_at") + .col::("updated_at") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.id)? + .bind(&self.channel_id)? + .bind(&self.guild_id)? + .bind(&self.author_id)? + .bind(&self.content)? + .bind(&self.timestamp)? + .bind(&self.edited_timestamp)? + .bind(&self.tts)? + .bind(&self.mentions)? + .bind(&self.mention_everyone)? + .bind(&self.embeds)? + .bind(&self.attachments)? + .bind(&self.reply_message_id)? + .bind(&self.application_id)? + .bind(&self.message_type)? + .bind(&self.thread_name)? + .bind(&self.auto_archive_duration)? + .bind(&self.created_at)? + .bind(&self.updated_at) + } +} + +impl SelectableRow for MessageRow {} + +impl WhereRow for MessageRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "id" => query.bind(&self.id)?, + "channel_id" => query.bind(&self.channel_id)?, + "guild_id" => query.bind(&self.guild_id)?, + "author_id" => query.bind(&self.author_id)?, + "content" => query.bind(&self.content)?, + "timestamp" => query.bind(&self.timestamp)?, + "edited_timestamp" => query.bind(&self.edited_timestamp)?, + "tts" => query.bind(&self.tts)?, + "mentions" => query.bind(&self.mentions)?, + "mention_everyone" => query.bind(&self.mention_everyone)?, + "embeds" => query.bind(&self.embeds)?, + "attachments" => query.bind(&self.attachments)?, + "reply_message_id" => query.bind(&self.reply_message_id)?, + "application_id" => query.bind(&self.application_id)?, + "message_type" => query.bind(&self.message_type)?, + "thread_name" => query.bind(&self.thread_name)?, + "auto_archive_duration" => query.bind(&self.auto_archive_duration)?, + "created_at" => query.bind(&self.created_at)?, + "updated_at" => query.bind(&self.updated_at)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for MessageRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self { + id: value.try_get("id")?, + channel_id: value.try_get("channel_id")?, + guild_id: value.try_get("guild_id")?, + author_id: value.try_get("author_id")?, + content: value.try_get("content")?, + timestamp: value.try_get("timestamp")?, + edited_timestamp: value.try_get("edited_timestamp")?, + tts: value.try_get("tts")?, + mentions: value.try_get("mentions")?, + mention_everyone: value.try_get("mention_everyone")?, + embeds: value.try_get("embeds")?, + attachments: value.try_get("attachments")?, + reply_message_id: value.try_get("reply_message_id")?, + application_id: value.try_get("application_id")?, + message_type: value.try_get("message_type")?, + thread_name: value.try_get("thread_name")?, + auto_archive_duration: value.try_get("auto_archive_duration")?, + created_at: value.try_get("created_at")?, + updated_at: value.try_get("updated_at")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/mod.rs b/cove-db/src/rows/mod.rs new file mode 100644 index 0000000..5366f3c --- /dev/null +++ b/cove-db/src/rows/mod.rs @@ -0,0 +1,45 @@ +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::select::SqlSelect; +use crate::part::sql_where::SqlWhere; + +pub mod user; + +pub trait TableRow { + type Error; + type PartialRow: PartialTableRow; + fn get_table_name() -> &'static str; +} + +pub trait PartialTableRow { + type Error; + type FullTableRow: TableRow; + fn get_table_name() -> &'static str { + Self::FullTableRow::get_table_name() + } +} + +pub trait InsertableRow: TableRow { + fn insert(&'_ self) -> SqlInsert; + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error>; +} + +pub trait SelectableRow: PartialTableRow { + fn select(&'_ self, selected_columns: Vec>) -> SqlSelect { + let mut select = SqlSelect::with_table(Self::get_table_name()); + for column in selected_columns { + select.add_column(column); + } + select + } +} + +pub trait WhereRow: PartialTableRow { + fn wheres(&'_ self, where_fn: impl Fn(SqlWhere) -> Result) -> Result { + let wheres = SqlWhere::new(); + let wheres = where_fn(wheres)?; + Ok(wheres) + } + + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error>; +} \ No newline at end of file diff --git a/cove-db/src/rows/nonce.rs b/cove-db/src/rows/nonce.rs new file mode 100644 index 0000000..56d4bfc --- /dev/null +++ b/cove-db/src/rows/nonce.rs @@ -0,0 +1,77 @@ +use anyhow::anyhow; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::id::SnowflakeID; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow}; + +struct NonceRow { + pub nonce: String, + pub channel_id: SnowflakeID, + pub author_id: SnowflakeID, + pub created_at: OffsetDateTime, + pub expires_at: OffsetDateTime, +} + +impl TableRow for NonceRow { + type Error = anyhow::Error; + + fn get_table_name() -> &'static str { + "pending_nonces" + } +} + +impl InsertableRow for NonceRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("nonce") + .col::("channel_id") + .col::("author_id") + .col::("created_at") + .col::("expires_at") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.nonce)? + .bind(&self.channel_id)? + .bind(&self.author_id)? + .bind(&self.created_at)? + .bind(&self.expires_at) + } +} + +impl SelectableRow for NonceRow {} + +impl WhereRow for NonceRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "nonce" => query.bind(&self.nonce)?, + "channel_id" => query.bind(&self.channel_id)?, + "author_id" => query.bind(&self.author_id)?, + "created_at" => query.bind(&self.created_at)?, + "expires_at" => query.bind(&self.expires_at)?, + _ => return Err(anyhow!("No column {col} exists for table attachments")) + } + } + Ok(query) + } +} + +impl TryFrom for NonceRow { + type Error = anyhow::Error; + + fn try_from(value: PgRow) -> Result { + Ok(Self { + nonce: value.try_get("nonce")?, + channel_id: value.try_get("channel_id")?, + author_id: value.try_get("author_id")?, + created_at: value.try_get("created_at")?, + expires_at: value.try_get("expires_at")?, + }) + } +} \ No newline at end of file diff --git a/cove-db/src/rows/user.rs b/cove-db/src/rows/user.rs new file mode 100644 index 0000000..6c15bae --- /dev/null +++ b/cove-db/src/rows/user.rs @@ -0,0 +1,205 @@ +use anyhow::anyhow; +use serde_json::Value; +use sqlx::postgres::PgRow; +use sqlx::Row; +use sqlx::types::time::OffsetDateTime; +use cove_net_common::id::SnowflakeID; +use crate::part::BindQuery; +use crate::part::insert::SqlInsert; +use crate::part::sql_where::SqlWhere; +use crate::rows::{InsertableRow, PartialTableRow, SelectableRow, TableRow, WhereRow}; +use crate::types::user_status::UserStatus; + +pub struct UserRow { + pub id: SnowflakeID, + pub username: String, + pub discriminator: String, + pub avatar_hash: Option, + pub email: String, + pub email_verified: bool, + pub password_hash: String, + pub mfa_enabled: bool, + pub mfa_secret: Option, + pub status: UserStatus, + pub public_flags: i64, + pub locale: String, + pub premium_since: Option, + pub premium_end: Option, + pub bot: bool, + pub bot_oauth_scopes: Value, + pub preferences: Value, + pub created_at: OffsetDateTime, + pub updated_at: OffsetDateTime, +} + +#[derive(Default)] +pub struct PartialUserRow { + pub id: Option, + pub username: Option, + pub discriminator: Option, + pub avatar_hash: Option>, + pub email: Option, + pub email_verified: Option, + pub password_hash: Option, + pub mfa_enabled: Option, + pub mfa_secret: Option>, + pub status: Option, + pub public_flags: Option, + pub locale: Option, + pub premium_since: Option>, + pub premium_end: Option>, + pub bot: Option, + pub bot_oauth_scopes: Option, + pub preferences: Option, + pub created_at: Option, + pub updated_at: Option, +} + +impl TableRow for UserRow { + type Error = anyhow::Error; + type PartialRow = PartialUserRow; + + fn get_table_name() -> &'static str { + "users" + } +} + +impl PartialTableRow for PartialUserRow { + type Error = anyhow::Error; + type FullTableRow = UserRow; + +} + +impl InsertableRow for UserRow { + fn insert(&'_ self) -> SqlInsert { + SqlInsert::with_table(Self::get_table_name()) + .col::("id") + .col::("username") + .col::("discriminator") + .opt_col::("avatar_hash") + .col::("email") + .col::("email_verified") + .col::("password_hash") + .col::("mfa_enabled") + .opt_col::("mfa_secret") + .col::("status") + .col::("public_flags") + .col::("locale") + .opt_col::("premium_since") + .opt_col::("premium_end") + .col::("bot") + .col::("bot_oauth_scopes") + .col::("preferences") + .col::("created_at") + .col::("updated_at") + } + + fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result, Self::Error> { + query.bind(&self.id)? + .bind(&self.username)? + .bind(&self.discriminator)? + .bind(&self.avatar_hash)? + .bind(&self.email)? + .bind(&self.email_verified)? + .bind(&self.password_hash)? + .bind(&self.mfa_enabled)? + .bind(&self.mfa_secret)? + .bind(&self.status)? + .bind(&self.public_flags)? + .bind(&self.locale)? + .bind(&self.premium_since)? + .bind(&self.premium_end)? + .bind(&self.bot)? + .bind(&self.bot_oauth_scopes)? + .bind(&self.preferences)? + .bind(&self.created_at)? + .bind(&self.updated_at) + } +} + +impl SelectableRow for PartialUserRow {} + +impl WhereRow for PartialUserRow { + fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result, Self::Error> { + let mut query = query; + for (_, col) in wheres.indexed_placeholders { + query = match col.as_str() { + "id" => query.bind(self.id.as_ref().unwrap())?, + "username" => query.bind(self.username.as_ref().unwrap())?, + "discriminator" => query.bind(self.discriminator.as_ref().unwrap())?, + "avatar_hash" => query.bind(self.avatar_hash.as_ref().unwrap())?, + "email" => query.bind(self.email.as_ref().unwrap())?, + "email_verified" => query.bind(self.email_verified.as_ref().unwrap())?, + "password_hash" => query.bind(self.password_hash.as_ref().unwrap())?, + "mfa_enabled" => query.bind(self.mfa_enabled.as_ref().unwrap())?, + "mfa_secret" => query.bind(self.mfa_secret.as_ref().unwrap())?, + "status" => query.bind(self.status.as_ref().unwrap())?, + "public_flags" => query.bind(self.public_flags.as_ref().unwrap())?, + "locale" => query.bind(self.locale.as_ref().unwrap())?, + "premium_since" => query.bind(self.premium_since.as_ref().unwrap())?, + "premium_end" => query.bind(self.premium_end.as_ref().unwrap())?, + "bot" => query.bind(self.bot.as_ref().unwrap())?, + "bot_oauth_scopes" => query.bind(self.bot_oauth_scopes.as_ref().unwrap())?, + "preferences" => query.bind(self.preferences.as_ref().unwrap())?, + "created_at" => query.bind(self.created_at.as_ref().unwrap())?, + "updated_at" => query.bind(self.updated_at.as_ref().unwrap())?, + _ => return Err(anyhow!("No column {col} exists for table users")) + } + } + Ok(query) + } +} + +impl TryFrom for UserRow { + type Error = anyhow::Error; + + fn try_from(row: PgRow) -> Result { + Ok(Self { + id: row.try_get("id")?, + username: row.try_get("username")?, + discriminator: row.try_get("discriminator")?, + avatar_hash: row.try_get("avatar_hash")?, + email: row.try_get("email")?, + email_verified: row.try_get("email_verified")?, + password_hash: row.try_get("password_hash")?, + mfa_enabled: row.try_get("mfa_enabled")?, + mfa_secret: row.try_get("mfa_secret")?, + status: row.try_get("status")?, + public_flags: row.try_get("public_flags")?, + locale: row.try_get("locale")?, + premium_since: row.try_get("premium_since")?, + premium_end: row.try_get("premium_end")?, + bot: row.try_get("bot")?, + bot_oauth_scopes: row.try_get("bot_oauth_scopes")?, + preferences: row.try_get("preferences")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }) + } +} + +impl From for PartialUserRow { + fn from(row: PgRow) -> Self { + Self { + id: row.try_get("id").ok(), + username: row.try_get("username").ok(), + discriminator: row.try_get("discriminator").ok(), + avatar_hash: row.try_get("avatar_hash").ok(), + email: row.try_get("email").ok(), + email_verified: row.try_get("email_verified").ok(), + password_hash: row.try_get("password_hash").ok(), + mfa_enabled: row.try_get("mfa_enabled").ok(), + mfa_secret: row.try_get("mfa_secret").ok(), + status: row.try_get("status").ok(), + public_flags: row.try_get("public_flags").ok(), + locale: row.try_get("locale").ok(), + premium_since: row.try_get("premium_since").ok(), + premium_end: row.try_get("premium_end").ok(), + bot: row.try_get("bot").ok(), + bot_oauth_scopes: row.try_get("bot_oauth_scopes").ok(), + preferences: row.try_get("preferences").ok(), + created_at: row.try_get("created_at").ok(), + updated_at: row.try_get("updated_at").ok(), + } + } +} \ No newline at end of file diff --git a/cove-db/src/types/mod.rs b/cove-db/src/types/mod.rs new file mode 100644 index 0000000..ebeb21a --- /dev/null +++ b/cove-db/src/types/mod.rs @@ -0,0 +1 @@ +pub mod user_status; \ No newline at end of file diff --git a/cove-db/src/types/user_status.rs b/cove-db/src/types/user_status.rs new file mode 100644 index 0000000..e8bb12d --- /dev/null +++ b/cove-db/src/types/user_status.rs @@ -0,0 +1,54 @@ +use sqlx::{Database, Decode, Encode, Postgres, Type, TypeInfo}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::PgTypeInfo; + +#[derive(Clone)] +pub enum UserStatus { + Online, + Idle, + Dnd, + Offline, + Invisible +} + +impl Encode<'_, Postgres> for UserStatus { + fn encode_by_ref<'q>(&self, buf: &mut ::ArgumentBuffer<'q>) -> Result { + <&str as Encode>::encode(self.clone().into(), buf) + } +} + +impl Decode<'_, Postgres> for UserStatus { + fn decode(value: ::ValueRef<'_>) -> Result { + match value.as_str()? { + "online" => Ok(UserStatus::Online), + "idle" => Ok(UserStatus::Idle), + "dnd" => Ok(UserStatus::Dnd), + "offline" => Ok(UserStatus::Offline), + "invisible" => Ok(UserStatus::Invisible), + _ => Err(BoxDynError::from("Unknown user status value")) + } + } +} + +impl Type for UserStatus { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("user_status") + } + + fn compatible(ty: &PgTypeInfo) -> bool { + PgTypeInfo::with_name("user_status").type_compatible(ty) + } +} + +impl Into<&str> for UserStatus { + fn into(self) -> &'static str { + match self { + UserStatus::Online => "online", + UserStatus::Idle => "idle", + UserStatus::Dnd => "dnd", + UserStatus::Offline => "offline", + UserStatus::Invisible => "invisible", + } + } +} \ No newline at end of file diff --git a/cove-db/system-migrations/201_nonce-management.sql b/cove-db/system-migrations/201_nonce-management.sql new file mode 100644 index 0000000..a3be75f --- /dev/null +++ b/cove-db/system-migrations/201_nonce-management.sql @@ -0,0 +1,21 @@ +INSERT INTO pgagent.pga_jobclass (jclname) + VALUES ('Cleanup Nonces'); + +INSERT INTO pgagent.pga_job (jobjclid, jobname, jobdesc, jobenabled, jobhostagent) +SELECT jcl.jclid, 'Cleanup Pending Nonces', 'Remove expired pending_nonces rows older than 5 minutes', true, '' +from pgagent.pga_jobclass jcl WHERE jclname='Cleanup Nonces'; + +INSERT INTO pgagent.pga_jobstep (jstjobid, jstname, jstdesc, jstenabled, jstkind, jstonerror, jstcode, jstdbname) +SELECT job.jobid, 'Perform Cleanup', 'Delete pending nonces', true, 's', 'f', $$DELETE FROM pending_nonces WHERE expires_at < NOW()$$, 'testdata' +FROM pgagent.pga_job job where jobname='Cleanup Pending Nonces'; + +INSERT INTO pgagent.pga_schedule (jscjobid, jscname, jscenabled, jscstart, jscminutes) +VALUES ( + (SELECT jobid + from pgagent.pga_job + where jobname = 'Cleanup Pending Nonces'), + 'Every 5 minutes', + true, + CURRENT_TIMESTAMP, -- start anytime + '{t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f}' -- every 5th minute + ); \ No newline at end of file diff --git a/cove-net/client/Cargo.toml b/cove-net/client/Cargo.toml new file mode 100644 index 0000000..a88122d --- /dev/null +++ b/cove-net/client/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "cove-net-client" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/cove-net/client/src/lib.rs b/cove-net/client/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/cove-net/client/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/cove-net/common/Cargo.toml b/cove-net/common/Cargo.toml new file mode 100644 index 0000000..3ed980b --- /dev/null +++ b/cove-net/common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "cove-net-common" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow.workspace = true +hex.workspace = true +rand = "0.10.0" +serde.workspace = true +serde_with.workspace = true +serde_json.workspace = true +sqlx.workspace = true \ No newline at end of file diff --git a/cove-net/common/src/guild/component/default_notification.rs b/cove-net/common/src/guild/component/default_notification.rs new file mode 100644 index 0000000..70366de --- /dev/null +++ b/cove-net/common/src/guild/component/default_notification.rs @@ -0,0 +1,53 @@ +use sqlx::error::BoxDynError; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; + +#[repr(i32)] +#[derive(Clone, Copy)] +pub enum DefaultMessageNotificationSetting { + All = 0, + MentionsOnly = 1, + None = 2, +} + + +impl Into for &DefaultMessageNotificationSetting { + fn into(self) -> i32 { + *self as i32 + } +} + +impl TryFrom for DefaultMessageNotificationSetting { + type Error = BoxDynError; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(Self::All), + 1 => Ok(Self::MentionsOnly), + 2 => Ok(Self::None), + _ => Err(format!("Unknown value {}", value).into()), + } + } +} + +impl Type for DefaultMessageNotificationSetting { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Postgres> for DefaultMessageNotificationSetting { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer<'_>) -> Result { + >::encode(self.into(), buf) + } +} + +impl Decode<'_, Postgres> for DefaultMessageNotificationSetting { + fn decode(value: ::ValueRef<'_>) -> Result { + >::decode(value).map(Self::try_from)? + } +} \ No newline at end of file diff --git a/cove-net/common/src/guild/component/explicit_filter.rs b/cove-net/common/src/guild/component/explicit_filter.rs new file mode 100644 index 0000000..9418421 --- /dev/null +++ b/cove-net/common/src/guild/component/explicit_filter.rs @@ -0,0 +1,52 @@ +use sqlx::error::BoxDynError; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; + +#[repr(i32)] +#[derive(Clone, Copy)] +pub enum ExplicitContentFilterSetting { + Disabled = 0, + MembersWithoutRole = 1, + All = 2 +} + +impl Into for &ExplicitContentFilterSetting { + fn into(self) -> i32 { + *self as i32 + } +} + +impl TryFrom for ExplicitContentFilterSetting { + type Error = BoxDynError; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(ExplicitContentFilterSetting::Disabled), + 1 => Ok(ExplicitContentFilterSetting::MembersWithoutRole), + 2 => Ok(ExplicitContentFilterSetting::All), + _ => Err(format!("Unknown value {}", value).into()), + } + } +} + +impl Type for ExplicitContentFilterSetting { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Postgres> for ExplicitContentFilterSetting { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer<'_>) -> Result { + >::encode(self.into(), buf) + } +} + +impl Decode<'_, Postgres> for ExplicitContentFilterSetting { + fn decode(value: ::ValueRef<'_>) -> Result { + >::decode(value).map(Self::try_from)? + } +} \ No newline at end of file diff --git a/cove-net/common/src/guild/component/mod.rs b/cove-net/common/src/guild/component/mod.rs new file mode 100644 index 0000000..406b6dd --- /dev/null +++ b/cove-net/common/src/guild/component/mod.rs @@ -0,0 +1,3 @@ +pub mod verification_level; +pub mod default_notification; +pub mod explicit_filter; \ No newline at end of file diff --git a/cove-net/common/src/guild/component/verification_level.rs b/cove-net/common/src/guild/component/verification_level.rs new file mode 100644 index 0000000..faa8323 --- /dev/null +++ b/cove-net/common/src/guild/component/verification_level.rs @@ -0,0 +1,56 @@ +use sqlx::error::BoxDynError; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; + +#[repr(i32)] +#[derive(Clone, Copy)] +pub enum VerificationLevelSetting { + None = 0, + Low = 1, + Medium = 2, + High = 3, + Highest = 4, +} + +impl Into for &VerificationLevelSetting { + fn into(self) -> i32 { + *self as i32 + } +} + +impl TryFrom for VerificationLevelSetting { + type Error = BoxDynError; + + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::Low), + 2 => Ok(Self::Medium), + 3 => Ok(Self::High), + 4 => Ok(Self::Highest), + _ => Err(format!("Unknown value {}", value).into()), + } + } +} + +impl Type for VerificationLevelSetting { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Postgres> for VerificationLevelSetting { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer<'_>) -> Result { + >::encode(self.into(), buf) + } +} + +impl Decode<'_, Postgres> for VerificationLevelSetting { + fn decode(value: ::ValueRef<'_>) -> Result { + >::decode(value).map(Self::try_from)? + } +} \ No newline at end of file diff --git a/cove-net/common/src/guild/mod.rs b/cove-net/common/src/guild/mod.rs new file mode 100644 index 0000000..519c53d --- /dev/null +++ b/cove-net/common/src/guild/mod.rs @@ -0,0 +1 @@ +pub mod component; \ No newline at end of file diff --git a/cove-net/common/src/id/message_type.rs b/cove-net/common/src/id/message_type.rs new file mode 100644 index 0000000..c2a48ed --- /dev/null +++ b/cove-net/common/src/id/message_type.rs @@ -0,0 +1,76 @@ +use std::fmt::Formatter; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use crate::id::types::channel::ChannelMessageType; +use crate::id::types::text::TextMessageType; + +#[repr(u8)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum MessageType { + Text(TextMessageType) = 0, + Channel(ChannelMessageType) = 1, + Guild = 2, + User = 3, +} + +impl std::fmt::Display for MessageType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + MessageType::Text(msg_type) => write!(f, "Text({})", msg_type), + MessageType::Channel(msg_type) => write!(f, "Channel({})", msg_type), + MessageType::Guild => write!(f, "Guild"), + MessageType::User => write!(f, "User"), + } + } +} + +impl Into for &MessageType { + fn into(self) -> u8 { + match self { + MessageType::Text(_) => 0, + MessageType::Channel(_) => 1, + MessageType::Guild => 2, + MessageType::User => 3, + } + } +} + +impl Into<[u8; 2]> for &MessageType { + fn into(self) -> [u8; 2] { + match self { + MessageType::Text(subtype) => [ + 0, subtype.into(), + ], + MessageType::Channel(subtype) => [ + 1, subtype.into(), + ], + MessageType::Guild => [2, 0], + MessageType::User => [3, 0], + } + } +} + +impl TryFrom<&[u8;26]> for MessageType { + type Error = anyhow::Error; + + fn try_from(value: &[u8; 26]) -> Result { + match value[0] { + 0 => { + match TextMessageType::try_from(value[1]) { + Ok(t) => Ok(MessageType::Text(t)), + Err(_) => Err(anyhow!("Unknown message subtype for Text: {}", value[1])) + } + } + 1 => { + match ChannelMessageType::try_from(value[1]) { + Ok(t) => Ok(MessageType::Channel(t)), + Err(_) => Err(anyhow!("Unknown message type for Channel: {}", value[1])) + } + }, + 2 => Ok(MessageType::Guild), + 3 => Ok(MessageType::User), + _ => Err(anyhow!("Unknown message type: {}", value[0])), + } + } +} + diff --git a/cove-net/common/src/id/mod.rs b/cove-net/common/src/id/mod.rs new file mode 100644 index 0000000..f862f83 --- /dev/null +++ b/cove-net/common/src/id/mod.rs @@ -0,0 +1,176 @@ +pub mod types; +pub mod message_type; + +/* +Snowflake ID Format + +Byte Idx.0 - Message Type +Byte Idx.1 - Message Subtype + +Bytes Idx.2-5 - Message Storage Location + +Bytes Idx.6-25 - Message ID + +Hexadecimal text encoding + +Type Location +| | +DE-AD-B33FCAFE-0102030405060708091011121314151617181920 + | | + Subtype Unique ID + */ +use std::fmt::{Display, Formatter}; +use std::mem::transmute; +use std::str::FromStr; +use anyhow::anyhow; +use hex::{FromHexError}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::{PgHasArrayType, PgTypeInfo}; +use crate::id::message_type::MessageType; + +#[repr(C, packed(1))] +#[derive(Debug, DeserializeFromStr, SerializeDisplay, Clone)] +pub struct SnowflakeID { + pub message_type: MessageType, + pub location: [u8;4], + pub id: [u8;20] +} + +impl Display for SnowflakeID { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(hex::encode::<[u8;2]>((&self.message_type).into()).as_str())?; + f.write_str(hex::encode(self.location).as_str())?; + f.write_str(hex::encode(self.id).as_str())?; + + Ok(()) + } +} + +impl FromStr for SnowflakeID { + type Err = anyhow::Error; + + fn from_str(input: &str) -> Result { + let mut hex = [0u8; 26]; + + let result = if input.len() != 52 { + if input.len() == 55 { + let mut chars = input.chars(); + if chars.nth(2).unwrap() == '-' && + chars.nth(2).unwrap() == '-' && + chars.nth(8).unwrap() == '-' { + let out = [&input[0..2], &input[3..5], &input[6..14], &input[15..]]; + hex::decode_to_slice(out.concat(), &mut hex) + } else { + return Err(anyhow!("Bad ID Format: {}", &input)) + } + } else { + return Err(anyhow!("Bad ID Format: {}", &input)) + } + } else { + hex::decode_to_slice(input, &mut hex) + }; + + match result { + Ok(_) => { + Self::from_bytes(hex) + } + Err(_) => { + Err(anyhow!("Bad ID Format: {}", &input)) + } + } + } +} + +impl Into<[u8;26]> for &SnowflakeID { + fn into(self) -> [u8;26] { + let message_type: [u8; 2] = (&self.message_type).into(); + let concat_buf: [&[u8]; 3] = [&message_type, &self.location, &self.id]; + let mut ret = [0u8; 26]; + concat_buf.concat().swap_with_slice(&mut ret); + ret + } +} + +impl Encode<'_, Postgres> for SnowflakeID { + fn encode_by_ref(&self, buf: &mut ::ArgumentBuffer<'_>) -> Result { + let bytes: [u8;26] = self.into(); + <&[u8] as Encode>::encode(&bytes, buf) + } + + fn size_hint(&self) -> usize { + 26 + } +} + +impl Decode<'_, Postgres> for SnowflakeID { + fn decode(value: ::ValueRef<'_>) -> Result { + let bytes = value.as_bytes()?; + let mut bytes_a = [0u8; 26]; + bytes_a.copy_from_slice(bytes); + + SnowflakeID::from_bytes(bytes_a).map_err(|err| BoxDynError::from(err)) + } +} + +impl PgHasArrayType for SnowflakeID { + fn array_type_info() -> PgTypeInfo { + <&[&[u8]] as Type>::type_info() + } +} + +impl Type for SnowflakeID { + fn type_info() -> ::TypeInfo { + <&[u8] as Type>::type_info() + } + fn compatible(ty: &::TypeInfo) -> bool { + <&[u8] as Type>::compatible(ty) + } +} + +impl SnowflakeID { + pub fn new_random(message_type: MessageType, location: [u8; 4]) -> SnowflakeID { + let mut id = [0u8; 20]; + rand::fill(&mut id); + + SnowflakeID { + message_type, + location, + id + } + } + + pub fn new_random_hex_loc(message_type: MessageType, location: &str) -> Result { + let mut loc_out = [0u8; 4]; + hex::decode_to_slice(location, &mut loc_out)?; + + let mut id = [0u8; 20]; + rand::fill(&mut id); + + Ok( + SnowflakeID { + message_type, + location: loc_out, + id + } + ) + } + + pub fn from_bytes(input: [u8; 26]) -> Result { + match MessageType::try_from(&input) { + Ok(_) => { + Ok(unsafe { transmute(input) }) + } + Err(e) => { + let out_err = format!("Failed to convert to SnowflakeID: {}", e); + Err(anyhow!(out_err)) + } + } + } + + pub unsafe fn from_bytes_unchecked(input: [u8; 26]) -> SnowflakeID { unsafe { + transmute(input) + } } +} \ No newline at end of file diff --git a/cove-net/common/src/id/types/channel.rs b/cove-net/common/src/id/types/channel.rs new file mode 100644 index 0000000..008f7c2 --- /dev/null +++ b/cove-net/common/src/id/types/channel.rs @@ -0,0 +1,75 @@ +use std::fmt::Formatter; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::PgValueRef; + +#[repr(u8)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum ChannelMessageType { + Text = 0, + Voice = 1, + Category = 2, + Thread = 3, +} + +impl Encode<'_, Postgres> for ChannelMessageType { + fn encode_by_ref<'q>(&self, buf: &mut ::ArgumentBuffer<'q>) -> Result { + let u8_self: u8 = self.into(); + >::encode(u8_self as i32, buf) + } +} + +impl Decode<'_, Postgres> for ChannelMessageType { + fn decode(value: PgValueRef<'_>) -> Result { + let value = >::decode(value)?; + Self::try_from(value as u8).map_err(|e| e.into()) + } +} + +impl Type for ChannelMessageType { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +impl std::fmt::Display for ChannelMessageType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + ChannelMessageType::Text => write!(f, "Text"), + ChannelMessageType::Voice => write!(f, "Voice"), + ChannelMessageType::Category => write!(f, "Category"), + ChannelMessageType::Thread => write!(f, "Thread"), + } + } +} + +impl Into for &ChannelMessageType { + fn into(self) -> u8 { + match self { + ChannelMessageType::Text => 0, + ChannelMessageType::Voice => 1, + ChannelMessageType::Category => 2, + ChannelMessageType::Thread => 3, + } + } +} + +impl TryFrom for ChannelMessageType { + type Error = anyhow::Error; + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(ChannelMessageType::Text), + 1 => Ok(ChannelMessageType::Voice), + 2 => Ok(ChannelMessageType::Category), + 3 => Ok(ChannelMessageType::Thread), + _ => Err(anyhow!("Unknown submessage type: {}", value)) + } + } +} diff --git a/cove-net/common/src/id/types/mod.rs b/cove-net/common/src/id/types/mod.rs new file mode 100644 index 0000000..9539508 --- /dev/null +++ b/cove-net/common/src/id/types/mod.rs @@ -0,0 +1,2 @@ +pub mod text; +pub mod channel; \ No newline at end of file diff --git a/cove-net/common/src/id/types/text.rs b/cove-net/common/src/id/types/text.rs new file mode 100644 index 0000000..bd2b634 --- /dev/null +++ b/cove-net/common/src/id/types/text.rs @@ -0,0 +1,72 @@ +use std::fmt::Formatter; +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::PgValueRef; + +#[repr(u8)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum TextMessageType { + Text = 0, + Reaction = 1, + Attachment = 2 +} + +impl Encode<'_, Postgres> for TextMessageType { + fn encode_by_ref<'q>(&self, buf: &mut ::ArgumentBuffer<'q>) -> Result { + let u8_self: u8 = self.into(); + >::encode(u8_self as i32, buf) + } +} + +impl Decode<'_, Postgres> for TextMessageType { + fn decode(value: PgValueRef<'_>) -> Result { + let value = >::decode(value)?; + Self::try_from(value as u8).map_err(|e| e.into()) + } +} + +impl Type for TextMessageType { + fn type_info() -> ::TypeInfo { + >::type_info() + } + + fn compatible(ty: &::TypeInfo) -> bool { + >::compatible(ty) + } +} + +impl std::fmt::Display for TextMessageType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + TextMessageType::Text => write!(f, "Text"), + TextMessageType::Reaction => write!(f, "Reaction"), + TextMessageType::Attachment => write!(f, "Attachment"), + } + } +} + +impl Into for &TextMessageType { + fn into(self) -> u8 { + match self { + TextMessageType::Text => 0, + TextMessageType::Reaction => 1, + TextMessageType::Attachment => 2 + } + } +} + +impl TryFrom for TextMessageType { + type Error = anyhow::Error; + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(TextMessageType::Text), + 1 => Ok(TextMessageType::Reaction), + 2 => Ok(TextMessageType::Attachment), + _ => Err(anyhow!("Unknown submessage type: {}", value)) + } + } +} + diff --git a/cove-net/common/src/lib.rs b/cove-net/common/src/lib.rs new file mode 100644 index 0000000..2f3abbe --- /dev/null +++ b/cove-net/common/src/lib.rs @@ -0,0 +1,3 @@ +pub mod id; +pub mod message; +pub mod guild; \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/account/login.rs b/cove-net/common/src/message/c2s/account/login.rs new file mode 100644 index 0000000..ac54541 --- /dev/null +++ b/cove-net/common/src/message/c2s/account/login.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; +use crate::message::c2s::ClientToServerMessage; + +#[derive(Deserialize)] +pub struct LoginMessage { + pub username: String, + pub password: String, +} + +impl ClientToServerMessage for LoginMessage { + type ServerToClientMessage = crate::message::s2c::account::login::LoginMessage; + + fn create_s2c_message(&self) -> Self::ServerToClientMessage { + Self::ServerToClientMessage { + username: self.username.clone(), + login_successful: false, + login_token: None + } + } +} + diff --git a/cove-net/common/src/message/c2s/account/mod.rs b/cove-net/common/src/message/c2s/account/mod.rs new file mode 100644 index 0000000..2c33a74 --- /dev/null +++ b/cove-net/common/src/message/c2s/account/mod.rs @@ -0,0 +1,2 @@ +pub mod login; +pub mod register; \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/account/register.rs b/cove-net/common/src/message/c2s/account/register.rs new file mode 100644 index 0000000..4f5a68a --- /dev/null +++ b/cove-net/common/src/message/c2s/account/register.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; +use crate::message::c2s::ClientToServerMessage; + +#[derive(Deserialize)] +pub struct RegisterMessage { + pub username: String, + pub email: String, + pub password: String, +} + +impl ClientToServerMessage for RegisterMessage { + type ServerToClientMessage = crate::message::s2c::account::register::RegisterMessage; + + fn create_s2c_message(&self) -> Self::ServerToClientMessage { + Self::ServerToClientMessage { + username: self.username.clone(), + user_already_exists: true, + user_created: false + } + } +} \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/mod.rs b/cove-net/common/src/message/c2s/mod.rs new file mode 100644 index 0000000..9335b5c --- /dev/null +++ b/cove-net/common/src/message/c2s/mod.rs @@ -0,0 +1,12 @@ +use crate::message::s2c::ServerToClientMessage; + +pub mod text; +pub mod account; + +pub trait ClientToServerMessage { + type ServerToClientMessage: ServerToClientMessage; + + /// This should return a default message that must be filled before being returned to the client + fn create_s2c_message(&self) -> Self::ServerToClientMessage; +} + diff --git a/cove-net/common/src/message/c2s/text/attachment.rs b/cove-net/common/src/message/c2s/text/attachment.rs new file mode 100644 index 0000000..a287d4d --- /dev/null +++ b/cove-net/common/src/message/c2s/text/attachment.rs @@ -0,0 +1,29 @@ +use serde::Deserialize; +use crate::id::message_type::MessageType; +use crate::id::SnowflakeID; +use crate::id::types::text::TextMessageType; +use crate::message::c2s::ClientToServerMessage; + +#[derive(Deserialize)] +pub struct AttachmentMessage { + pub file_name: String, + pub content_type: String, + pub file_size: u64, + pub file_contents: Vec, +} + +impl ClientToServerMessage for AttachmentMessage { + type ServerToClientMessage = crate::message::s2c::text::attachment::AttachmentMessage; + + fn create_s2c_message(&self) -> Self::ServerToClientMessage { + Self::ServerToClientMessage { + id: SnowflakeID::new_random_hex_loc( + MessageType::Text(TextMessageType::Attachment), + "beefcafe" + ).unwrap(), + file_name: self.file_name.clone(), + file_size: self.file_size, + content_type: self.content_type.clone(), + } + } +} \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/text/mod.rs b/cove-net/common/src/message/c2s/text/mod.rs new file mode 100644 index 0000000..621b422 --- /dev/null +++ b/cove-net/common/src/message/c2s/text/mod.rs @@ -0,0 +1,3 @@ +pub mod attachment; +pub mod reaction; +pub mod text; \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/text/reaction.rs b/cove-net/common/src/message/c2s/text/reaction.rs new file mode 100644 index 0000000..cb161e9 --- /dev/null +++ b/cove-net/common/src/message/c2s/text/reaction.rs @@ -0,0 +1,26 @@ +use serde::Deserialize; +use crate::id::message_type::MessageType; +use crate::id::SnowflakeID; +use crate::id::types::text::TextMessageType; +use crate::message::c2s::ClientToServerMessage; + +#[derive(Deserialize)] +pub struct ReactionMessage { + pub emoji_id: SnowflakeID, + pub message_id: SnowflakeID, +} + +impl ClientToServerMessage for ReactionMessage { + type ServerToClientMessage = crate::message::s2c::text::reaction::ReactionMessage; + + fn create_s2c_message(&self) -> Self::ServerToClientMessage { + Self::ServerToClientMessage { + id: SnowflakeID::new_random_hex_loc( + MessageType::Text(TextMessageType::Reaction), + "beefcafe" + ).unwrap(), + emoji_id: self.emoji_id.clone(), + message_id: self.message_id.clone(), + } + } +} \ No newline at end of file diff --git a/cove-net/common/src/message/c2s/text/text.rs b/cove-net/common/src/message/c2s/text/text.rs new file mode 100644 index 0000000..d0b81dd --- /dev/null +++ b/cove-net/common/src/message/c2s/text/text.rs @@ -0,0 +1,91 @@ +use serde::{Deserialize, Serialize}; +use sqlx::{Database, Decode, Encode, Postgres, Type}; +use sqlx::encode::IsNull; +use sqlx::error::BoxDynError; +use sqlx::postgres::{PgHasArrayType, PgTypeInfo}; +use crate::id::message_type::MessageType; +use crate::id::SnowflakeID; +use crate::id::types::text::TextMessageType; +use crate::message::c2s::ClientToServerMessage; +use crate::message::component::channel::ChannelIdMessageComponent; +use crate::message::component::guild::GuildIdMessageComponent; + +#[derive(Deserialize)] +pub struct TextMessage { + pub content: String, + pub channel_id: SnowflakeID, + pub guild_id: Option, + pub attachments: Option>, + pub reply_to_message_id: Option, + pub tts: bool, + pub embeds: Option> +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct TextEmbed { + pub title: Option, + pub content: Option, + pub color: Option, +} + +impl PgHasArrayType for TextEmbed { + fn array_type_info() -> PgTypeInfo { + sqlx::types::JsonValue::array_type_info() + } +} + +impl Decode<'_, Postgres> for TextEmbed { + fn decode(value: ::ValueRef<'_>) -> Result { + let value = sqlx::types::JsonValue::decode(value)?; + serde_json::from_value(value).map_err(|e| e.into()) + } +} + +impl Encode<'_, Postgres> for TextEmbed { + fn encode_by_ref<'q>(&self, buf: &mut ::ArgumentBuffer<'q>) -> Result { + let value = serde_json::to_value(self)?; + value.encode(buf) + } +} + +impl Type for TextEmbed { + fn type_info() -> PgTypeInfo { + sqlx::types::JsonValue::type_info() + } + + fn compatible(ty: &PgTypeInfo) -> bool { + sqlx::types::JsonValue::compatible(ty) + } +} + +impl ClientToServerMessage for TextMessage { + type ServerToClientMessage = crate::message::s2c::text::text::TextMessage; + + fn create_s2c_message(&self) -> Self::ServerToClientMessage { + Self::ServerToClientMessage { + id: SnowflakeID::new_random_hex_loc( + MessageType::Text(TextMessageType::Text), + "beefcafe" + ).unwrap(), + content: self.content.clone(), + channel_id: self.channel_id.clone(), + guild_id: self.guild_id.clone(), + attachments: self.attachments.clone(), + reply_to_message_id: self.reply_to_message_id.clone(), + tts: self.tts, + embeds: self.embeds.clone(), + } + } +} + +impl GuildIdMessageComponent for TextMessage { + fn get_guild_id(&self) -> &Option { + &self.guild_id + } +} + +impl ChannelIdMessageComponent for TextMessage { + fn get_channel_id(&self) -> &SnowflakeID { + &self.channel_id + } +} \ No newline at end of file diff --git a/cove-net/common/src/message/component/channel.rs b/cove-net/common/src/message/component/channel.rs new file mode 100644 index 0000000..420f37e --- /dev/null +++ b/cove-net/common/src/message/component/channel.rs @@ -0,0 +1,5 @@ +use crate::id::SnowflakeID; + +pub trait ChannelIdMessageComponent { + fn get_channel_id(&self) -> &SnowflakeID; +} \ No newline at end of file diff --git a/cove-net/common/src/message/component/guild.rs b/cove-net/common/src/message/component/guild.rs new file mode 100644 index 0000000..3854f64 --- /dev/null +++ b/cove-net/common/src/message/component/guild.rs @@ -0,0 +1,5 @@ +use crate::id::SnowflakeID; + +pub trait GuildIdMessageComponent { + fn get_guild_id(&self) -> &Option; +} \ No newline at end of file diff --git a/cove-net/common/src/message/component/mod.rs b/cove-net/common/src/message/component/mod.rs new file mode 100644 index 0000000..1eb0d76 --- /dev/null +++ b/cove-net/common/src/message/component/mod.rs @@ -0,0 +1,2 @@ +pub mod guild; +pub mod channel; \ No newline at end of file diff --git a/cove-net/common/src/message/mod.rs b/cove-net/common/src/message/mod.rs new file mode 100644 index 0000000..578a76a --- /dev/null +++ b/cove-net/common/src/message/mod.rs @@ -0,0 +1,3 @@ +pub mod c2s; +pub mod s2c; +pub mod component; \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/account/login.rs b/cove-net/common/src/message/s2c/account/login.rs new file mode 100644 index 0000000..878671a --- /dev/null +++ b/cove-net/common/src/message/s2c/account/login.rs @@ -0,0 +1,13 @@ +use serde::Serialize; +use crate::message::s2c::ServerToClientMessage; + +#[derive(Serialize)] +pub struct LoginMessage { + pub username: String, + pub login_successful: bool, + pub login_token: Option, +} + +impl ServerToClientMessage for LoginMessage { + type ClientToServerMessage = crate::message::c2s::account::login::LoginMessage; +} \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/account/mod.rs b/cove-net/common/src/message/s2c/account/mod.rs new file mode 100644 index 0000000..2c33a74 --- /dev/null +++ b/cove-net/common/src/message/s2c/account/mod.rs @@ -0,0 +1,2 @@ +pub mod login; +pub mod register; \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/account/register.rs b/cove-net/common/src/message/s2c/account/register.rs new file mode 100644 index 0000000..611c4d4 --- /dev/null +++ b/cove-net/common/src/message/s2c/account/register.rs @@ -0,0 +1,13 @@ +use serde::Serialize; +use crate::message::s2c::ServerToClientMessage; + +#[derive(Serialize)] +pub struct RegisterMessage { + pub username: String, + pub user_already_exists: bool, + pub user_created: bool, +} + +impl ServerToClientMessage for RegisterMessage { + type ClientToServerMessage = crate::message::c2s::account::register::RegisterMessage; +} \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/mod.rs b/cove-net/common/src/message/s2c/mod.rs new file mode 100644 index 0000000..d327a71 --- /dev/null +++ b/cove-net/common/src/message/s2c/mod.rs @@ -0,0 +1,8 @@ +use crate::message::c2s::ClientToServerMessage; + +pub mod text; +pub mod account; + +pub trait ServerToClientMessage { + type ClientToServerMessage: ClientToServerMessage; +} \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/text/attachment.rs b/cove-net/common/src/message/s2c/text/attachment.rs new file mode 100644 index 0000000..50dbd3f --- /dev/null +++ b/cove-net/common/src/message/s2c/text/attachment.rs @@ -0,0 +1,15 @@ +use serde::Serialize; +use crate::id::SnowflakeID; +use crate::message::s2c::ServerToClientMessage; + +#[derive(Serialize)] +pub struct AttachmentMessage { + pub id: SnowflakeID, + pub file_name: String, + pub content_type: String, + pub file_size: u64, +} + +impl ServerToClientMessage for AttachmentMessage { + type ClientToServerMessage = crate::message::c2s::text::attachment::AttachmentMessage; +} \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/text/mod.rs b/cove-net/common/src/message/s2c/text/mod.rs new file mode 100644 index 0000000..621b422 --- /dev/null +++ b/cove-net/common/src/message/s2c/text/mod.rs @@ -0,0 +1,3 @@ +pub mod attachment; +pub mod reaction; +pub mod text; \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/text/reaction.rs b/cove-net/common/src/message/s2c/text/reaction.rs new file mode 100644 index 0000000..7b008bb --- /dev/null +++ b/cove-net/common/src/message/s2c/text/reaction.rs @@ -0,0 +1,14 @@ +use serde::Serialize; +use crate::id::SnowflakeID; +use crate::message::s2c::ServerToClientMessage; + +#[derive(Serialize)] +pub struct ReactionMessage { + pub id: SnowflakeID, + pub emoji_id: SnowflakeID, + pub message_id: SnowflakeID, +} + +impl ServerToClientMessage for ReactionMessage { + type ClientToServerMessage = crate::message::c2s::text::reaction::ReactionMessage; +} \ No newline at end of file diff --git a/cove-net/common/src/message/s2c/text/text.rs b/cove-net/common/src/message/s2c/text/text.rs new file mode 100644 index 0000000..cf5017d --- /dev/null +++ b/cove-net/common/src/message/s2c/text/text.rs @@ -0,0 +1,20 @@ +use serde::Serialize; +use crate::id::SnowflakeID; +use crate::message::c2s::text::text::TextEmbed; +use crate::message::s2c::ServerToClientMessage; + +#[derive(Serialize)] +pub struct TextMessage { + pub id: SnowflakeID, + pub content: String, + pub channel_id: SnowflakeID, + pub guild_id: Option, + pub attachments: Option>, + pub reply_to_message_id: Option, + pub tts: bool, + pub embeds: Option> +} + +impl ServerToClientMessage for TextMessage { + type ClientToServerMessage = crate::message::c2s::text::text::TextMessage; +} \ No newline at end of file diff --git a/cove-net/server/Cargo.toml b/cove-net/server/Cargo.toml new file mode 100644 index 0000000..9ec6181 --- /dev/null +++ b/cove-net/server/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cove-net-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +http-body-util.workspace = true +hyper.workspace = true +hyper-util.workspace = true +scc.workspace = true +tokio.workspace = true +serde_json.workspace = true + +cove-net-common.workspace = true +cove-db.workspace = true \ No newline at end of file diff --git a/cove-net/server/src/lib.rs b/cove-net/server/src/lib.rs new file mode 100644 index 0000000..5b0e1fb --- /dev/null +++ b/cove-net/server/src/lib.rs @@ -0,0 +1,138 @@ +pub mod message; + +use cove_db::CoveDB; +use std::any::{Any, TypeId}; +use std::collections::VecDeque; +use std::net::{IpAddr, SocketAddr}; +use http_body_util::{BodyExt, Full}; +use hyper::body::{Bytes, Incoming}; +use hyper::server::conn::http2; +use hyper::service::Service; +use hyper::{Request, Response, StatusCode}; +use std::pin::Pin; +use std::sync::Arc; +use anyhow::Error; +use async_trait::async_trait; +use hyper::http::request::Parts; +use tokio::net::TcpListener; +use hyper_util::rt::TokioIo; +use crate::message::handlers::base::handler::CoveRequestHandler; +use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware}; +use crate::message::handlers::Handler; +use crate::message::middleware::auth::AuthTokenMiddlewareData; + +#[derive(Clone)] +pub struct CoveServer { + pub root_handler: Arc, + pub socket_addr: SocketAddr +} + +impl CoveServer { + pub async fn new(ip_address: IpAddr, port: u16, root_handler: Arc) -> Result { + let socket_addr = SocketAddr::new(ip_address, port); + + Ok(CoveServer { + root_handler, + socket_addr + }) + } + + pub async fn run(&self) -> Result<(), Error> { + let tcp_listener = TcpListener::bind(self.socket_addr).await?; + + loop { + let (stream, _) = tcp_listener.accept().await?; + let io = TokioIo::new(stream); + + let cloned = self.clone(); + tokio::task::spawn(async move { + if let Err(err) = http2::Builder::new(TokioExecutor) + .serve_connection(io, cloned) + .await + { + eprintln!("Error: {}", err); + } + }); + } + } +} + +impl Service> for CoveServer { + type Response = Response>; + type Error = Error; + type Future = Pin> + Send>>; + + fn call(&self, request: Request) -> Self::Future { + let root_handler = self.root_handler.clone(); + Box::pin(async move { + let uri = request.uri().clone(); + let mut path_parts = uri.path().trim_start_matches('/').split('/').collect::>(); + println!("{:?} {:?}", uri, path_parts); + let (parts, body) = request.into_parts(); + let body = body.collect().await?.to_bytes(); + match root_handler.handle_request_internal(parts, body, &mut path_parts, &mut AssociatedDataMap::new()).await { + Ok(response) => Ok(response), + Err(err) => { + eprintln!("Error: {}", err); + let res = Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("An error has occured, please try again later".into())?; + Ok(res) + } + } + }) + } +} + +#[derive(Clone)] +pub struct TokioExecutor; + +impl hyper::rt::Executor for TokioExecutor +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + fn execute(&self, future: F) { + tokio::task::spawn(future); + } +} + +pub struct RootHandler; +#[async_trait] +impl CoveRequestHandler for RootHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + if let Some(data) = associated_data.get_data::() { + println!("{}", data.user_id) + } + + if let Some(db) = associated_data.get_data::() { + println!("Got db: {:?}", db.db.type_id()); + } + + Ok(Response::new(Full::new(Bytes::from("Root handler called")))) + } +} + +pub struct DatabaseMiddleware { + db: Arc +} + +impl DatabaseMiddleware { + pub fn new(db: Arc) -> DatabaseMiddleware { + DatabaseMiddleware { db } + } +} + +pub struct DatabaseMiddlewareData { + pub db: Arc +} + +#[async_trait] +impl CoveRequestMiddleware for DatabaseMiddleware { + async fn transform_request(&self, _request_parts: &Parts, _body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result>>, Error> { + associated_data.insert(Box::new(DatabaseMiddlewareData { + db: self.db.clone() + })); + Ok(None) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/account/login.rs b/cove-net/server/src/message/handlers/account/login.rs new file mode 100644 index 0000000..b10c37a --- /dev/null +++ b/cove-net/server/src/message/handlers/account/login.rs @@ -0,0 +1,31 @@ +use hyper::body::Buf; +use http_body_util::BodyExt; +use std::collections::VecDeque; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::account::login::LoginMessage; +use cove_net_common::message::c2s::ClientToServerMessage; +use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler}; +use crate::message::handlers::base::middleware::AssociatedDataMap; + +pub struct LoginMessageHandler; + +#[async_trait] +impl CoveRequestHandler for LoginMessageHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + let msg = self.body_to_message(body).await?; + Ok(Response::new( + serde_json::to_string(&msg.create_s2c_message())?.into() + )) + } +} +#[async_trait] +impl CoveBodyDeserializer for LoginMessageHandler { + async fn body_to_message(&self, body: Bytes) -> Result { + Ok(serde_json::from_slice(&body)?) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/account/mod.rs b/cove-net/server/src/message/handlers/account/mod.rs new file mode 100644 index 0000000..2c33a74 --- /dev/null +++ b/cove-net/server/src/message/handlers/account/mod.rs @@ -0,0 +1,2 @@ +pub mod login; +pub mod register; \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/account/register.rs b/cove-net/server/src/message/handlers/account/register.rs new file mode 100644 index 0000000..44cb0df --- /dev/null +++ b/cove-net/server/src/message/handlers/account/register.rs @@ -0,0 +1,31 @@ +use hyper::body::Buf; +use http_body_util::BodyExt; +use std::collections::VecDeque; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::account::register::RegisterMessage; +use cove_net_common::message::c2s::ClientToServerMessage; +use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler}; +use crate::message::handlers::base::middleware::AssociatedDataMap; + +pub struct RegisterMessageHandler; + +#[async_trait] +impl CoveRequestHandler for RegisterMessageHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + let msg = self.body_to_message(body).await?; + Ok(Response::new( + serde_json::to_string(&msg.create_s2c_message())?.into() + )) + } +} +#[async_trait] +impl CoveBodyDeserializer for RegisterMessageHandler { + async fn body_to_message(&self, body: Bytes) -> Result { + Ok(serde_json::from_slice(&body)?) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/base/handler.rs b/cove-net/server/src/message/handlers/base/handler.rs new file mode 100644 index 0000000..323e0a8 --- /dev/null +++ b/cove-net/server/src/message/handlers/base/handler.rs @@ -0,0 +1,19 @@ +use std::collections::VecDeque; +use anyhow::{Error}; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::ClientToServerMessage; +use crate::message::handlers::base::middleware::{AssociatedDataMap}; + +#[async_trait] +pub trait CoveRequestHandler: Send + Sync { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error>; +} + +#[async_trait] +pub trait CoveBodyDeserializer where Self: CoveRequestHandler { + async fn body_to_message(&self, body: Bytes) -> Result; +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/base/middleware.rs b/cove-net/server/src/message/handlers/base/middleware.rs new file mode 100644 index 0000000..d881afc --- /dev/null +++ b/cove-net/server/src/message/handlers/base/middleware.rs @@ -0,0 +1,45 @@ +use std::any::{Any, TypeId}; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use crate::message::StdHashMap; + +#[async_trait] +pub trait CoveRequestMiddleware { + async fn transform_request(&self, request: &Parts, body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result>>, Error>; +} + +pub struct AssociatedDataMap { + internal_data: StdHashMap>, +} + +impl AssociatedDataMap { + pub fn new() -> AssociatedDataMap { + AssociatedDataMap { + internal_data: StdHashMap::new(), + } + } + + pub fn insert (&mut self, data: Box) { + self.internal_data.insert(TypeId::of::(), data); + } + + pub fn get_data (&self) -> Option<&T> { + self.internal_data.get(&TypeId::of::()).map(|data| data.downcast_ref().unwrap()) + } + + pub fn has_data(&self) -> bool { + self.internal_data.contains_key(&TypeId::of::()) + } +} + +impl IntoIterator for AssociatedDataMap { + type Item = (TypeId, Box); + type IntoIter = std::collections::hash_map::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.internal_data.into_iter() + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/base/mod.rs b/cove-net/server/src/message/handlers/base/mod.rs new file mode 100644 index 0000000..5977d09 --- /dev/null +++ b/cove-net/server/src/message/handlers/base/mod.rs @@ -0,0 +1,2 @@ +pub mod middleware; +pub mod handler; \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/mod.rs b/cove-net/server/src/message/handlers/mod.rs new file mode 100644 index 0000000..ce660db --- /dev/null +++ b/cove-net/server/src/message/handlers/mod.rs @@ -0,0 +1,117 @@ +use std::any::{Any, TypeId}; +use std::collections::VecDeque; +use std::sync::Arc; +use anyhow::{anyhow, Error}; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use scc::HashMap; +use crate::message::handlers::base::handler::CoveRequestHandler; +use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware}; +use crate::message::StdHashMap; + +pub mod base; +pub mod account; +pub mod text; + +#[macro_export] +macro_rules! register_routes { + // Base case + // Matches: register_routes!(&mut h, ) + ($parent:expr, ) => { + async { Ok::<_, anyhow::Error>(()) } + }; + + // Recursive w/ nested routes + // Matches: "path" => Handler => { ... } ... + ($parent:expr, $path:literal => $handler:expr => $nested:tt $(, $($rest:tt)*)?) => { + async { + let mut handler = Handler::new(Box::new($handler)); + + register_routes!(&mut handler, $nested).await?; + $parent.register_subhandler($path.into(), Arc::new(handler)).await?; + + register_routes!($parent, $($($rest)*)?).await?; + Ok::<_, anyhow::Error>(()) + } + }; + + // Recursive: w/o nested routes + // Matches: "path" => Handler ... + ($parent:expr, $path:literal => $handler:expr $(, $($rest:tt)*)?) => { + async { + let mut handler = Handler::new(Box::new($handler)); + + $parent.register_subhandler($path.into(), Arc::new(handler)).await?; + + register_routes!($parent, $($($rest)*)?).await?; + Ok::<_, anyhow::Error>(()) + } + }; + + ($parent:expr, { $($inner:tt)* }) => { + register_routes!($parent, $($inner)*) + }; +} + +pub struct Handler { + handler: Box, + handlers: HashMap>, + middleware: StdHashMap>, +} + +impl Handler { + pub fn new(handler: Box) -> Self { + Handler { + handler, + handlers: HashMap::new(), + middleware: StdHashMap::new(), + } + } + + pub async fn register_subhandler(&mut self, path: String, handler: Arc) -> Result<(), Error> { + if self.handlers.contains_async(&path).await { + Err(anyhow!("Handler {} already exists!", path)) + } else { + self.handlers.upsert_async(path.clone(), handler).await; + Ok(()) + } + } + + pub async fn add_middleware(&mut self, middleware: T) -> Result<(), Error> { + let typeid = TypeId::of::(); + if self.middleware.contains_key(&typeid) { + return Err(anyhow!("Middleware {:?} already exists!", typeid)) + } + self.middleware.insert(typeid, Box::new(middleware)); + Ok(()) + } + pub async fn handle_request_internal(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + for (_tid, middleware) in self.middleware.iter() { + match middleware.transform_request(&request_parts, &body, associated_data).await? { + None => {} + Some(res) => { + return Ok(res); + } + } + } + + match path.pop_front() { + None => { + self.handler.handle_request(request_parts, body, path, associated_data).await + } + Some(path_part) => { + match self.handlers.get_async::(&path_part.to_string()).await { + None => { + self.handler.handle_request(request_parts, body, path, associated_data).await + } + Some(handler) => { + Box::pin(handler.handle_request_internal(request_parts, body, path, associated_data)).await + } + } + } + } + } + +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/text/attachment.rs b/cove-net/server/src/message/handlers/text/attachment.rs new file mode 100644 index 0000000..e04d339 --- /dev/null +++ b/cove-net/server/src/message/handlers/text/attachment.rs @@ -0,0 +1,32 @@ +use hyper::body::Buf; +use http_body_util::BodyExt; +use std::collections::VecDeque; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::ClientToServerMessage; +use cove_net_common::message::c2s::text::attachment::AttachmentMessage; +use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler}; +use crate::message::handlers::base::middleware::AssociatedDataMap; + +pub struct AttachmentMessageHandler; + +#[async_trait] +impl CoveRequestHandler for AttachmentMessageHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + let msg = self.body_to_message(body).await?; + Ok(Response::new( + serde_json::to_string(&msg.create_s2c_message())?.into() + )) + } +} + +#[async_trait] +impl CoveBodyDeserializer for AttachmentMessageHandler { + async fn body_to_message(&self, body: Bytes) -> Result { + Ok(serde_json::from_slice(&body)?) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/text/mod.rs b/cove-net/server/src/message/handlers/text/mod.rs new file mode 100644 index 0000000..621b422 --- /dev/null +++ b/cove-net/server/src/message/handlers/text/mod.rs @@ -0,0 +1,3 @@ +pub mod attachment; +pub mod reaction; +pub mod text; \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/text/reaction.rs b/cove-net/server/src/message/handlers/text/reaction.rs new file mode 100644 index 0000000..482ccd9 --- /dev/null +++ b/cove-net/server/src/message/handlers/text/reaction.rs @@ -0,0 +1,31 @@ +use hyper::body::Buf; +use http_body_util::BodyExt; +use std::collections::VecDeque; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::ClientToServerMessage; +use cove_net_common::message::c2s::text::reaction::ReactionMessage; +use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler}; +use crate::message::handlers::base::middleware::AssociatedDataMap; + +pub struct ReactionMessageHandler; + +#[async_trait] +impl CoveRequestHandler for ReactionMessageHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + let msg = self.body_to_message(body).await?; + Ok(Response::new( + serde_json::to_string(&msg.create_s2c_message())?.into() + )) + } +} +#[async_trait] +impl CoveBodyDeserializer for ReactionMessageHandler { + async fn body_to_message(&self, body: Bytes) -> Result { + Ok(serde_json::from_slice(&body)?) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/handlers/text/text.rs b/cove-net/server/src/message/handlers/text/text.rs new file mode 100644 index 0000000..aa5587e --- /dev/null +++ b/cove-net/server/src/message/handlers/text/text.rs @@ -0,0 +1,31 @@ +use hyper::body::Buf; +use http_body_util::BodyExt; +use std::collections::VecDeque; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response}; +use hyper::http::request::Parts; +use cove_net_common::message::c2s::ClientToServerMessage; +use cove_net_common::message::c2s::text::text::TextMessage; +use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler}; +use crate::message::handlers::base::middleware::AssociatedDataMap; + +pub struct TextMessageHandler; + +#[async_trait] +impl CoveRequestHandler for TextMessageHandler { + async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result>, Error> { + let msg = self.body_to_message(body).await?; + Ok(Response::new( + serde_json::to_string(&msg.create_s2c_message())?.into() + )) + } +} +#[async_trait] +impl CoveBodyDeserializer for TextMessageHandler { + async fn body_to_message(&self, body: Bytes) -> Result { + Ok(serde_json::from_slice(&body)?) + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/middleware/action_source.rs b/cove-net/server/src/message/middleware/action_source.rs new file mode 100644 index 0000000..545bb45 --- /dev/null +++ b/cove-net/server/src/message/middleware/action_source.rs @@ -0,0 +1,95 @@ +use std::str::FromStr; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::{Full}; +use hyper::body::{Bytes}; +use hyper::{Response, StatusCode}; +use hyper::http::request::Parts; +use serde_json::Value; +use cove_net_common::id::SnowflakeID; +use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware}; + +pub struct ActionSourceMiddleware; + +pub struct ActionSourceMiddlewareData { + pub channel_id: SnowflakeID, + pub guild_id: Option, +} + +#[async_trait] +impl CoveRequestMiddleware for ActionSourceMiddleware { + async fn transform_request(&self, _request_parts: &Parts, body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result>>, Error> { + match serde_json::from_slice::(body)? { + Value::Object(map) => { + if !map.contains_key("channel_id") { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Channel ID missing for AS middleware endpoint"))?; + return Ok(Some(res)); + } + + let guild_id = if let Some(guild_id) = map.get("guild_id") { + if let Some(guild_id) = guild_id.as_str() { + match SnowflakeID::from_str(guild_id) { + Ok(id) => Some(id), + Err(_err) => { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Bad Guild ID for AS middleware endpoint"))?; + return Ok(Some(res)) + } + } + } else { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Bad Guild ID for AS middleware endpoint"))?; + return Ok(Some(res)) + } + } else { + None + }; + + let channel_id = if let Some(channel_id) = map.get("channel_id") { + if let Some(channel_id) = channel_id.as_str() { + match SnowflakeID::from_str(channel_id) { + Ok(id) => id, + Err(_err) => { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Bad Channel ID for AS middleware endpoint"))?; + return Ok(Some(res)); + } + } + } else { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Bad Channel ID for AS middleware endpoint"))?; + return Ok(Some(res)); + } + } else { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Channel ID missing for AS middleware endpoint"))?; + return Ok(Some(res)); + }; + + associated_data.insert( + Box::new( + ActionSourceMiddlewareData { + channel_id, + guild_id + } + ) + ); + + Ok(None) + }, + _ => { + let res = Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Full::from("Non JSON Object request sent to AS middleware endpoint"))?; + Ok(Some(res)) + } + } + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/middleware/auth.rs b/cove-net/server/src/message/middleware/auth.rs new file mode 100644 index 0000000..3a8d3ec --- /dev/null +++ b/cove-net/server/src/message/middleware/auth.rs @@ -0,0 +1,60 @@ +use std::any::TypeId; +use std::time::SystemTime; +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::{Bytes, Incoming}; +use hyper::{Request, Response, StatusCode}; +use hyper::http::request::Parts; +use cove_net_common::id::message_type::MessageType; +use cove_net_common::id::SnowflakeID; +use cove_net_common::id::types::text::TextMessageType; +use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware}; + +pub struct AuthTokenMiddlewareData { + pub user_id: SnowflakeID, + pub expires_at: SystemTime, + pub refresh_time: SystemTime, +} + +pub struct AuthTokenMiddleware; + +#[async_trait] +impl CoveRequestMiddleware for AuthTokenMiddleware { + async fn transform_request(&self, request_parts: &Parts, body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result>>, Error> { + match request_parts.headers.get("Auth") { + None => { + let response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("Missing auth header".into())?; + Ok(Some(response)) + } + Some(auth_header) => { + match auth_header.to_str() { + Ok(auth_str) => { + if auth_str.starts_with("Bearer") { + let _token = auth_str[6..auth_str.len()].trim(); + associated_data.insert(Box::new(AuthTokenMiddlewareData { + user_id: SnowflakeID::new_random_hex_loc(MessageType::Text(TextMessageType::Text), "beefcafe")?, + expires_at: SystemTime::now(), + refresh_time: SystemTime::now(), + })); + Ok(None) + } else { + let response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("Invalid auth header".into())?; + Ok(Some(response)) + } + } + Err(_) => { + let response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body("Invalid auth header".into())?; + Ok(Some(response)) + } + } + } + } + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/middleware/guildpermissions.rs b/cove-net/server/src/message/middleware/guildpermissions.rs new file mode 100644 index 0000000..af0ed16 --- /dev/null +++ b/cove-net/server/src/message/middleware/guildpermissions.rs @@ -0,0 +1,54 @@ +/* +Middleware, runs after the auth middleware, on user action endpoints. +If the action taking place is in a guild, this will add the permissions of that user to the +AssociatedData map. + */ +use anyhow::Error; +use async_trait::async_trait; +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::{Response, StatusCode}; +use hyper::http::request::Parts; +use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware}; +use crate::message::middleware::action_source::ActionSourceMiddlewareData; +use crate::message::middleware::auth::AuthTokenMiddlewareData; + +pub struct GuildPermissionsMiddleware; + +#[async_trait] +impl CoveRequestMiddleware for GuildPermissionsMiddleware { + async fn transform_request(&self, _request_parts: &Parts, _body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result>>, Error> { + match associated_data.get_data::() { + None => { + let res = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .body(Full::from("Unauthenticated request made to a GP endpoint"))?; + Ok( + Some(res) + ) + } + Some(auth) => { + match associated_data.get_data::() { + None => { + let res = Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Full::from("GP endpoint called without AS middleware"))?; + Ok( + Some(res) + ) + } + Some(action_source) => { + match &action_source.guild_id { + None => { + Ok(None) + } + Some(guild_id) => { + todo!("database lookup for guild permission") + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/cove-net/server/src/message/middleware/mod.rs b/cove-net/server/src/message/middleware/mod.rs new file mode 100644 index 0000000..bb11f4c --- /dev/null +++ b/cove-net/server/src/message/middleware/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod guildpermissions; +pub mod action_source; \ No newline at end of file diff --git a/cove-net/server/src/message/mod.rs b/cove-net/server/src/message/mod.rs new file mode 100644 index 0000000..059af58 --- /dev/null +++ b/cove-net/server/src/message/mod.rs @@ -0,0 +1,4 @@ +pub mod handlers; +pub mod middleware; + +pub use std::collections::HashMap as StdHashMap; \ No newline at end of file