diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a00ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target + +.env diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7f4820 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2370 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[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 = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[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-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "halfbrown" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8588661a8607108a5ca69cab034063441a0413a0b041c13618a7dd348021ef6f" +dependencies = [ + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "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", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier 0.6.1", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "payphone" +version = "4.0.0" +dependencies = [ + "async-trait", + "clap", + "colored", + "dotenvy", + "include_dir", + "log", + "once_cell", + "scc", + "scylla", + "thiserror 2.0.12", + "time", + "tokio", + "twilight-gateway", + "twilight-http", + "twilight-model", + "twilight-util", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[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 = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[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 = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +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.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_pcg" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +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.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 1.0.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scc" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" +dependencies = [ + "sdd", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scylla" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42cf9feea170a110c5644c013a4dc790c24c60dc802f43df2ca69acae5112a4" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "chrono", + "dashmap", + "futures", + "hashbrown 0.15.4", + "itertools", + "rand", + "rand_pcg", + "scylla-cql", + "smallvec", + "socket2 0.5.10", + "thiserror 2.0.12", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "scylla-cql" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5139a271deeb8b3b8118c28d19bb3519421ec4a131e3a39533503c526d415dc2" +dependencies = [ + "byteorder", + "bytes", + "chrono", + "itertools", + "lz4_flex", + "scylla-macros", + "snap", + "stable_deref_trait", + "thiserror 2.0.12", + "tokio", + "uuid", + "yoke", +] + +[[package]] +name = "scylla-macros" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162aed3aa5b6985d121d9e7e4137efebd49645dee204962b2e9ab85176349119" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-json" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2bcf6c6e164e81bc7a5d49fc6988b3d515d9e8c07457d7b74ffb9324b9cd40" +dependencies = [ + "getrandom 0.2.16", + "halfbrown", + "ref-cast", + "serde", + "serde_json", + "simdutf8", + "value-trait", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +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 = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e" +dependencies = [ + "base64", + "bytes", + "fastrand", + "futures-core", + "futures-sink", + "http", + "httparse", + "ring", + "rustls-pki-types", + "rustls-platform-verifier 0.5.3", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +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 = "twilight-gateway" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ef55467bcf6a2958162766fd0b72f33112e36971b37f9ec09b86d4fd0f7d1" +dependencies = [ + "bitflags", + "fastrand", + "flate2", + "futures-core", + "futures-sink", + "serde", + "serde_json", + "tokio", + "tokio-websockets", + "tracing", + "twilight-gateway-queue", + "twilight-http", + "twilight-model", +] + +[[package]] +name = "twilight-gateway-queue" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c962bd4693da0a215abe6b431fd73eb87111f49c431b87951780f9f7985002" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-http" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9a2176638fd8bfeb867e7a2f644fee0db905eba6f7decfdcd75c8171109b74" +dependencies = [ + "brotli-decompressor", + "fastrand", + "http", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "simd-json", + "tokio", + "tracing", + "twilight-http-ratelimiting", + "twilight-model", + "twilight-validate", +] + +[[package]] +name = "twilight-http-ratelimiting" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36945d949920d6bb6aef30547e06ea645eefaa5575a14f56da608bf09d07ec8" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-model" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191e2efa051dfbd9bed4c9f6bd3f5e007bda909c687a1db2760371a3d566617d" +dependencies = [ + "bitflags", + "serde", + "serde-value", + "serde_repr", + "time", +] + +[[package]] +name = "twilight-util" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5777d71286611c1639021f5f6e6b40300eb8da278449ac1cf6da01c94053425" +dependencies = [ + "twilight-model", + "twilight-validate", +] + +[[package]] +name = "twilight-validate" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f8d106028ede53708526364b03318bbf846babe146e3ff9e39821a0ca25ff4" +dependencies = [ + "twilight-model", +] + +[[package]] +name = "twox-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "value-trait" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9170e001f458781e92711d2ad666110f153e4e50bfd5cbd02db6547625714187" +dependencies = [ + "float-cmp", + "halfbrown", + "itoa", + "ryu", +] + +[[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 = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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 = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.2", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +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.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1b5c33e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "payphone" +version = "4.0.0" +edition = "2024" + +[dependencies] +async-trait = "0.1.88" +clap = { version = "4.5.42", features = ["derive", "env"] } +colored = "3.0.0" +dotenvy = "0.15.7" +include_dir = "0.7.4" +log = "0.4.27" +once_cell = "1.21.3" +scc = "2.3.4" +scylla = "1.3.1" +thiserror = "2.0.12" +time = { version = "0.3.41", features = ["formatting", "local-offset", "macros"] } +tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] } +twilight-gateway = "0.16.0" +twilight-http = { version = "0.16.0", features = ["simd-json"] } +twilight-model = "0.16.0" +twilight-util = { version = "0.16.0", features = ["builder"] } diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..48826cb --- /dev/null +++ b/compose.yml @@ -0,0 +1,44 @@ +services: + scylla-node-1: + image: scylladb/scylla:latest + container_name: scylla-node-1 + command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1 + ports: + - "9042:9042" # CQL + volumes: + - scylla-data-1:/var/lib/scylla + networks: + scylla-net: + ipv4_address: 172.20.0.2 + + scylla-node-2: + image: scylladb/scylla:latest + container_name: scylla-node-2 + command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1 + volumes: + - scylla-data-2:/var/lib/scylla + networks: + scylla-net: + ipv4_address: 172.20.0.3 + + scylla-node-3: + image: scylladb/scylla:latest + container_name: scylla-node-3 + command: --smp 1 --memory 750M --overprovisioned 1 --seeds=scylla-node-1 + volumes: + - scylla-data-3:/var/lib/scylla + networks: + scylla-net: + ipv4_address: 172.20.0.4 + +volumes: + scylla-data-1: + scylla-data-2: + scylla-data-3: + +networks: + scylla-net: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/24 diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e68b5c4 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,5 @@ +#[derive(clap::Parser)] +pub struct Args { + #[arg(env = "PAYPHONE_TOKEN")] + pub token: String, +} diff --git a/src/commands/about.rs b/src/commands/about.rs new file mode 100644 index 0000000..636b888 --- /dev/null +++ b/src/commands/about.rs @@ -0,0 +1,217 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use twilight_model::channel::message::Embed; +use twilight_util::builder::embed::{ + EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder, EmbedFooterBuilder, ImageSource, +}; + +use crate::{ + commands::{COMMANDS, PREFIX, get_command_by_name}, + constants::{BOT_AVATAR, BOT_NAME, COLOR_RED, COLOR_WHITE}, + framework::{ + commands::{ + Command, CommandError, + arguments::{Argument, NeedyString, ParseArguments}, + context::CommandContext, + }, + interactions::{InteractionError, components::Button, context::InteractionContext}, + utils::EscapeDiscordMarkdown, + }, + utils::ToTitleCase, +}; + +pub struct HelpCommand; + +#[async_trait] +impl Command for HelpCommand { + fn name(&self) -> &'static str { + "help" + } + + fn arguments(&self) -> &'static [Argument] { + &[Argument { + name: "command", + is_optional: true, + is_greedy: false, + }] + } + + fn description(&self) -> &'static str { + "Lists all commands and shows information about them individually." + } + + async fn execute(&self, ctx: &CommandContext, args: &str) -> Result<(), CommandError> { + let command_name: Option = ParseArguments::parse(args).unwrap(); + + if let Some(command_name) = command_name { + if let Some(command) = get_command_by_name(&command_name) { + ctx.send() + .embed(get_command_help_embed(&command)) + .component(HelpGoBackButton, 1) + .component(HelpSupportLinkButton, 1) + .await + .inspect_err(|e| log::error!("Failed to send message! {e}")) + .map_err(|_| CommandError::MessageSendingFailed)?; + } else { + ctx.send() + .embed(get_command_not_found_embed(command_name.0)) + .component(HelpGoBackButton, 1) + .component(HelpSupportLinkButton, 1) + .await + .map_err(|_| CommandError::MessageSendingFailed)?; + } + } else { + ctx.send() + .embed(get_all_command_help_embed()) + .component(HelpWebsiteLinkButton, 1) + .component(HelpSupportLinkButton, 1) + .component(HelpDonateLinkButton, 1) + .await + .map_err(|_| CommandError::MessageSendingFailed)?; + } + + Ok(()) + } +} + +fn get_command_help_embed(command: &Arc) -> Embed { + let name = format!("{PREFIX}{}", command.name()); + let arguments = command + .arguments() + .iter() + .map(|arg| arg.to_string()) + .collect::>() + .join(" "); + + let usage = format!("{name} {arguments}"); + + EmbedBuilder::new() + .author( + EmbedAuthorBuilder::new(BOT_NAME) + .icon_url(ImageSource::url(BOT_AVATAR).unwrap()) + .build(), + ) + .thumbnail(ImageSource::url(BOT_AVATAR).unwrap()) + .color(COLOR_WHITE) + .title(format!("{} Command", command.name().to_title_case())) + .description(command.description()) + .field(EmbedFieldBuilder::new("Usage", format!("`{usage}`")).build()) + .field(EmbedFieldBuilder::new( + "Aliases", + if !command.aliases().is_empty() { + command + .aliases() + .iter() + .map(|alias| format!("`{PREFIX}{alias}`")) + .collect::>() + .join(", ") + } else { + String::from("*This command doesn't have aliases.*") + }, + )) + .footer(EmbedFooterBuilder::new(format!( + "Use {PREFIX}help to get help about a specific command." + ))) + .build() +} + +fn get_all_command_help_embed() -> Embed { + EmbedBuilder::new() + .author(EmbedAuthorBuilder::new(BOT_NAME).icon_url(ImageSource::url(BOT_AVATAR).unwrap())) + .title("Pick up the phone and start making friends!") + .description(format!("Here is the list of all available commands. You can see more details about a specific command with `{PREFIX}help `.")) + .thumbnail(ImageSource::url(BOT_AVATAR).unwrap()) + .color(COLOR_WHITE) + .field(EmbedFieldBuilder::new("Commands", get_commands_table()).build()) + .footer(EmbedFooterBuilder::new(format!("v{} - Use {PREFIX}help to get more information about a specific command.", env!("CARGO_PKG_VERSION")))) + .build() +} + +fn get_commands_table() -> String { + let mut table = String::new(); + + for command in COMMANDS.iter() { + table.push_str(&format!("{:<12}", command.name())); + } + + format!("```\n{table}\n```") +} + +fn get_command_not_found_embed(name: String) -> Embed { + EmbedBuilder::new() + .color(COLOR_RED) + .author(EmbedAuthorBuilder::new(BOT_NAME).icon_url(ImageSource::url(BOT_AVATAR).unwrap())) + .title("That's not a command") + .description(format!("`{}` is not a valid command name (nor alias). If you put it with the prefix on, take the prefix off before trying again. For example, if you did `p.help p.call`, do `p.help call` instead.\n\nIf you still can't figure it out, press the button below to go back to the full list. Everything's there!", name.escape())) + .thumbnail(ImageSource::url(BOT_AVATAR).unwrap()) + .build() +} + +pub struct HelpGoBackButton; + +#[async_trait] +impl Button for HelpGoBackButton { + fn label(&self) -> Option<&'static str> { + Some("← Go back to commands list") + } + + async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> { + ctx + .original_message() + .expect( + "Buttons always have the original message in the interaction context, this one didn't (got None)" + ) + .edit() + .embed(get_all_command_help_embed()) + .component(HelpWebsiteLinkButton, 1) + .component(HelpSupportLinkButton, 1) + .component(HelpDonateLinkButton, 1) + .await + .inspect_err(|e| log::error!("An error occurred while attempting to edit the original message of an interaction. {e:?}")) + .map_err( + |_| InteractionError::OriginalMessageEditingFailed + )?; + + Ok(()) + } +} + +pub struct HelpSupportLinkButton; + +#[async_trait] +impl Button for HelpSupportLinkButton { + fn label(&self) -> Option<&'static str> { + Some("Get help") + } + + fn url(&self) -> Option<&'static str> { + Some("https://discord.gg/9daSsrfTmE") + } +} + +pub struct HelpWebsiteLinkButton; + +#[async_trait] +impl Button for HelpWebsiteLinkButton { + fn label(&self) -> Option<&'static str> { + Some("Website") + } + + fn url(&self) -> Option<&'static str> { + Some("https://payphone.lat/") + } +} + +pub struct HelpDonateLinkButton; + +#[async_trait] +impl Button for HelpDonateLinkButton { + fn label(&self) -> Option<&'static str> { + Some("Donate") + } + + fn url(&self) -> Option<&'static str> { + Some("https://ko-fi.com/payphonedev") + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..c09af01 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,59 @@ +mod about; + +use once_cell::sync::Lazy; +use std::sync::Arc; +use twilight_http::Client; +use twilight_model::{gateway::payload::incoming::MessageCreate, oauth::Application}; + +use crate::framework::{ + commands::{context::CommandContext, get_command_parts, Command}, handlers::Handlers, interactions::{components::Button, registry::Registry} +}; + +const PREFIX: &str = "pd."; + +static COMMANDS: Lazy>> = Lazy::new(|| vec![Arc::new(about::HelpCommand)]); + +pub fn get_command_by_name(name: &str) -> Option> { + for command in COMMANDS.iter() { + if command + .identifiers() + .contains(&name.to_ascii_lowercase().as_str()) + { + return Some(command.clone()); + } + } + + None +} + +pub async fn execute_command( + event: MessageCreate, + handlers: Arc +) { + let parts = get_command_parts(PREFIX, &event.content); + + if let Some(parts) = parts { + if let Some(command) = get_command_by_name(parts.name) { + let _ = command + .execute( + &CommandContext::new(&event, handlers), + parts.args, + ) + .await + .inspect_err(|e| log::error!("Failed to execute command! {e:?}")); + } + } +} + +pub fn register_default_components(registry: &Registry) { + let components = vec![Arc::new(about::HelpGoBackButton)]; + + for component in components { + registry.register( + component.id().expect( + "All components registered by default must have an ID, there's one which didn't", + ), + component, + ); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..02503a5 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,5 @@ +pub const BOT_NAME: &str = "Payphone"; +pub const BOT_AVATAR: &str = "https://images-ext-1.discordapp.net/external/BymicERFXlYKd52uNU2ihrbUoSm2e5hV3Ae7XY1436o/%3Fsize%3D1024/https/cdn.discordapp.com/avatars/1212940413795827763/f4b7180bb467b035d6e2e6f458e93b7c.png?format=webp&quality=lossless&width=1058&height=1058"; + +pub const COLOR_WHITE: u32 = 0xE2D9D0; +pub const COLOR_RED: u32 = 0xFF8787; diff --git a/src/database/migrations/0001_init.cql b/src/database/migrations/0001_init.cql new file mode 100644 index 0000000..f47af7b --- /dev/null +++ b/src/database/migrations/0001_init.cql @@ -0,0 +1,152 @@ +CREATE KEYSPACE IF NOT EXISTS payphone + WITH replication = { + 'class': 'NetworkTopologyStrategy', + 'replication_factor': 3 +}; + +CREATE TABLE IF NOT EXISTS payphone.users ( + user_id BIGINT PRIMARY KEY, + + name TEXT, + email TEXT, + + is_masked BOOLEAN, + is_moderator BOOLEAN, + + joined_at TIMESTAMP, + last_seen_at TIMESTAMP, + is_supporter_since TIMESTAMP, + is_supporter_until TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS payphone.calls ( + call_id BIGINT PRIMARY KEY, + + started_at TIMESTAMP, + finished_at TIMESTAMP, +); + +CREATE TABLE IF NOT EXISTS payphone.channels ( + channel_id BIGINT PRIMARY KEY, + server_id BIGINT, + + name TEXT, + + is_searching BOOLEAN, + + recently_called_channel_ids LIST +); + +CREATE TABLE IF NOT EXISTS payphone.participants ( + call_id BIGINT, + user_id BIGINT, + + user_name TEXT, + user_avatar_url TEXT, + user_mask TEXT, + + PRIMARY KEY (call_id, user_id) +); + +-- TTL 30 days. +CREATE TABLE IF NOT EXISTS payphone.messages ( + call_id BIGINT, + message_id BIGINT, + author_id BIGINT, + + author_name TEXT, + author_avatar_url TEXT, + author_mask ASCII, + + content TEXT, + attachments LIST, + sticker BIGINT, + + replying_to_id BIGINT, + + is_deleted BOOLEAN, + is_edited BOOLEAN, + is_censored BOOLEAN, + + PRIMARY KEY (call_id, message_id) +); + +CREATE TABLE IF NOT EXISTS payphone.servers ( + server_id BIGINT PRIMARY KEY, + + name TEXT, + prefix TEXT, +); + +CREATE TABLE IF NOT EXISTS payphone.reports ( + report_id BIGINT, + + message_author_id BIGINT, + message_author_name TEXT, + message_author_avatar_url TEXT, + message_author_mask ASCII, + + message_id BIGINT, + message_content TEXT, + message_attachments LIST, + message_sticker BIGINT, + + message_replying_to_id BIGINT, + message_replying_to_content TEXT, + message_replying_to_attachments LIST, + message_replying_to_sticker BIGINT, + + created_at TIMESTAMP, + reviewed_at TIMESTAMP, + + created_by_ids SET, + + reviewed_by_id BIGINT, + resolution ASCII, + + PRIMARY KEY (message_author_id, report_id) +); + +CREATE TABLE IF NOT EXISTS payphone.user_incidents ( + user_id BIGINT, + incident_id BIGINT, + + -- ban, warning + type ASCII, + + call_id BIGINT, + report_id BIGINT, + + message_id BIGINT, + message_content TEXT, + message_attachments LIST, + message_sticker BIGINT, + + message_replying_to_id BIGINT, + message_replying_to_content TEXT, + message_replying_to_attachments LIST, + message_replying_to_sticker BIGINT, + + reason TEXT, + + created_by_id BIGINT, + + created_at TIMESTAMP, + expires_at TIMESTAMP, + duration ASCII, + + PRIMARY KEY (user_id) +); + +CREATE TABLE IF NOT EXISTS payphone.server_incidents ( + server_id BIGINT PRIMARY KEY, + incident_id BIGINT, + + reason TEXT, + + created_by_id BIGINT, + + created_at TIMESTAMP, + expires_at TIMESTAMP, + duration ASCII +); \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..9162913 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,93 @@ +use include_dir::{Dir, include_dir}; +use scylla::{ + client::{session::Session, session_builder::SessionBuilder}, + errors::ExecutionError, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DatabaseError { + #[error("A connection was attempted to be established to the database, but it failed")] + ConnectionFailed, + + #[error("The database connection was established but the migrations could not be applied")] + MigrationFailed, + + #[error("The migration file was invalidly formatted (not UTF-8)")] + MigrationFileInvalid(String), +} + +static MIGRATIONS: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/database/migrations"); + +pub async fn init(uri: impl ToString) -> Result { + let session = SessionBuilder::new() + .known_node(uri.to_string()) + .build() + .await + .inspect_err(|e| log::error!("Could not connect to the database! {e:?}")) + .map_err(|_| DatabaseError::ConnectionFailed)?; + + execute_file( + &session, + r#" + CREATE KEYSPACE IF NOT EXISTS migrations + WITH replication = { + 'class': 'NetworkTopologyStrategy', + 'replication_factor': 3 + }; + + CREATE TABLE IF NOT EXISTS migrations.applied ( + name TEXT PRIMARY KEY, + applied_at TIMESTAMP + ); + "# + .into(), + ) + .await + .inspect(|_| log::info!("The migrations keyspace was successfully set up")) + .inspect_err(|e| log::error!("Could not set up migrations keyspace and table! {e:?}")) + .map_err(|_| DatabaseError::MigrationFailed)?; + + for file in MIGRATIONS.files() { + execute_file( + &session, + file.contents_utf8() + .ok_or(DatabaseError::MigrationFileInvalid( + file.path().to_string_lossy().to_string(), + ))? + .to_string(), + ) + .await + .inspect_err(|e| log::error!("Could not migrate {}! {e:?}", file.path().to_str().unwrap())) + .map_err(|_| DatabaseError::MigrationFailed)?; + } + + Ok(session) +} + +#[derive(Debug, Error)] +#[error("An error occurred while executing a statement in the file")] +struct FileExecutionError { + pub statement_index: usize, + pub error: ExecutionError, +} + +async fn execute_file(session: &Session, file: String) -> Result<(), FileExecutionError> { + let statements: Vec<&str> = file.split(";").collect(); + + for (statement_index, statement) in statements.iter().enumerate() { + if statement.trim().is_empty() { + continue; + } + + session + .query_unpaged(format!("{};", statement.trim()), ()) + .await + .map_err(|error| FileExecutionError { + error, + statement_index, + })?; + } + + Ok(()) +} diff --git a/src/discord/events/core.rs b/src/discord/events/core.rs new file mode 100644 index 0000000..95e551a --- /dev/null +++ b/src/discord/events/core.rs @@ -0,0 +1,13 @@ +use twilight_model::gateway::payload::incoming::Ready; + +pub async fn handle_on_ready(event: Ready) { + log::info!( + "Ready! Logged in as {}#{:>04}", + event.user.name, + event.user.discriminator + ); +} + +pub async fn handle_on_reconnect() { + log::info!("The gateway asked for a reconnection"); +} diff --git a/src/discord/events/interactions.rs b/src/discord/events/interactions.rs new file mode 100644 index 0000000..ed49678 --- /dev/null +++ b/src/discord/events/interactions.rs @@ -0,0 +1,11 @@ +use std::sync::Arc; + +use twilight_model::gateway::payload::incoming::InteractionCreate; + +use crate::framework::handlers::Handlers; + +pub async fn handle_on_interaction_create(event: InteractionCreate, handlers: Arc) { + if let Err(e) = handlers.registry.execute(event, handlers.clone()).await { + log::error!("An error occurred while processing the interaction! {e:?}"); + } +} diff --git a/src/discord/events/messages.rs b/src/discord/events/messages.rs new file mode 100644 index 0000000..8676e9c --- /dev/null +++ b/src/discord/events/messages.rs @@ -0,0 +1,12 @@ +use std::sync::Arc; + +use twilight_model::{gateway::payload::incoming::MessageCreate}; + +use crate::{commands, framework::{handlers::Handlers}}; + +pub fn handle_on_message_create( + event: MessageCreate, + handlers: Arc +) -> impl Future { + commands::execute_command(event, handlers) +} diff --git a/src/discord/events/mod.rs b/src/discord/events/mod.rs new file mode 100644 index 0000000..cb5817b --- /dev/null +++ b/src/discord/events/mod.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; + +use twilight_gateway::Event; + +use crate::framework::handlers::Handlers; + +mod core; +mod interactions; +mod messages; + +pub async fn handle_event(event: Event, handlers: Arc) { + log::trace!("{event:?}"); + + match event { + Event::Ready(event) => core::handle_on_ready(*event).await, + Event::GatewayReconnect => core::handle_on_reconnect().await, + Event::MessageCreate(event) => { + messages::handle_on_message_create(*event, handlers).await + } + Event::InteractionCreate(event) => { + interactions::handle_on_interaction_create(*event, handlers).await + } + _ => {} + } +} diff --git a/src/discord/mod.rs b/src/discord/mod.rs new file mode 100644 index 0000000..a9970c2 --- /dev/null +++ b/src/discord/mod.rs @@ -0,0 +1 @@ +pub mod events; diff --git a/src/framework/commands/arguments.rs b/src/framework/commands/arguments.rs new file mode 100644 index 0000000..63414cc --- /dev/null +++ b/src/framework/commands/arguments.rs @@ -0,0 +1,222 @@ +use std::{fmt, ops::{Deref, DerefMut}}; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ArgumentParsingError { + #[error("This argument was missing in the arguments array")] + MissingArgument, + + #[error("An argument was there but it could not be parsed into the specified value")] + InvalidArgument, +} + +pub trait FromArguments: Sized { + fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError>; +} + +impl FromArguments for String { + fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> { + if !args.is_empty() { + match args.split_once(" ") { + Some((arg, rest)) => Ok((arg.to_string(), rest)), + None => Ok((args.to_string(), "")), + } + } else { + Ok((args.to_string(), "")) + } + } +} + +pub struct GreedyString(pub String); + +impl FromArguments for GreedyString { + fn from_args<'a>(args: &str) -> Result<(Self, &str), ArgumentParsingError> { + let value = GreedyString(args.to_string()); + + Ok((value, "")) + } +} + +impl Deref for GreedyString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GreedyString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Display for GreedyString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub struct NeedyString(pub String); + +impl Deref for NeedyString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for NeedyString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Display for NeedyString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl FromArguments for NeedyString { + fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> { + if !args.is_empty() { + match args.split_once(" ") { + Some((arg, rest)) => Ok((Self(arg.to_string()), rest)), + None => Ok((Self(args.to_string()), "")), + } + } else { + Err(ArgumentParsingError::MissingArgument) + } + } +} + +impl FromArguments for bool { + fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> { + let arg: &str; + let rest: &str; + + match args.split_once(" ") { + Some((a, b)) => { + arg = a; + rest = b; + } + None => { + arg = args; + rest = ""; + } + } + + match arg { + "y" | "yes" | "on" | "true" | "t" | "1" => Ok((true, rest)), + "n" | "no" | "off" | "false" | "f" | "0" => Ok((false, rest)), + _ => Err(ArgumentParsingError::InvalidArgument), + } + } +} + +impl FromArguments for Option +where + T: FromArguments, +{ + fn from_args(args: &str) -> Result<(Self, &str), ArgumentParsingError> { + match T::from_args(args) { + Ok((result, rest)) => Ok((Some(result), rest)), + Err(_) => Ok((None, args)), + } + } +} + +pub trait ParseArguments: Sized { + fn parse(args: &str) -> Result; +} + +impl ParseArguments for () { + fn parse(_: &str) -> Result { + Ok(()) + } +} + +impl ParseArguments for A +where + A: FromArguments, +{ + fn parse(args: &str) -> Result { + let (a, _) = A::from_args(args)?; + + Ok(a) + } +} + +impl ParseArguments for (A,) +where + A: FromArguments, +{ + fn parse(args: &str) -> Result { + let (a, _) = A::from_args(args)?; + + Ok((a,)) + } +} + +impl ParseArguments for (A, B) +where + A: FromArguments, + B: FromArguments, +{ + fn parse(args: &str) -> Result { + let (a, args) = A::from_args(args)?; + let (b, _) = B::from_args(args)?; + + Ok((a, b)) + } +} + +impl ParseArguments for (A, B, C) +where + A: FromArguments, + B: FromArguments, + C: FromArguments, +{ + fn parse(args: &str) -> Result { + let (a, args) = A::from_args(args)?; + let (b, args) = B::from_args(args)?; + let (c, _) = C::from_args(args)?; + + Ok((a, b, c)) + } +} + +pub struct Argument { + pub name: &'static str, + pub is_optional: bool, + pub is_greedy: bool, +} + +impl fmt::Display for Argument { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut argument = String::new(); + + if self.is_optional { + argument.push('['); + } else { + argument.push('<'); + } + + argument.push_str(self.name); + + if self.is_greedy { + argument.push_str("..."); + } + + if self.is_optional { + argument.push(']'); + } else { + argument.push('>'); + } + + write!(f, "{argument}") + } +} diff --git a/src/framework/commands/context.rs b/src/framework/commands/context.rs new file mode 100644 index 0000000..e2daf8b --- /dev/null +++ b/src/framework/commands/context.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; + +use twilight_http::Client; +use twilight_model::{gateway::payload::incoming::MessageCreate, oauth::Application}; + +use crate::framework::{commands::messages::MessageSender, handlers::Handlers, interactions::registry::Registry}; + +pub struct CommandContext<'a> { + pub event: &'a MessageCreate, + pub handlers: Arc +} + +impl<'a> CommandContext<'a> { + pub fn new( + event: &'a MessageCreate, + handlers: Arc, + ) -> Self { + Self { + event, + handlers + } + } + + pub fn send(&self) -> MessageSender { + MessageSender::new( + self.event.channel_id, + self.handlers.clone() + ) + } +} diff --git a/src/framework/commands/messages.rs b/src/framework/commands/messages.rs new file mode 100644 index 0000000..6a3a0f0 --- /dev/null +++ b/src/framework/commands/messages.rs @@ -0,0 +1,252 @@ +use std::{ + future::{Future, IntoFuture}, + pin::Pin, + sync::Arc, +}; + +use twilight_http::{Error, Response, response::marker::EmptyBody}; +use twilight_model::{ + channel::{ + Message, + message::{Component as TwilightComponent, Embed}, + }, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::{ + Id, + marker::{ChannelMarker, InteractionMarker}, + }, +}; +use twilight_util::builder::InteractionResponseDataBuilder; + +use crate::framework::{ + handlers::Handlers, + interactions::components::{ActionRow, Component}, +}; + +pub struct MessageSender { + channel_id: Id, + + handlers: Arc, + + content: Option, + embeds: Vec, + + // Row index, component + components: Vec<(u8, Arc)>, +} + +impl MessageSender { + pub fn new(channel_id: Id, handlers: Arc) -> Self { + Self { + channel_id, + handlers, + content: None, + components: vec![], + embeds: vec![], + } + } + + pub fn content(mut self, content: impl ToString) -> Self { + self.content = Some(content.to_string()); + + self + } + + pub fn embed(mut self, embed: Embed) -> Self { + self.embeds.push(embed); + + self + } + + pub fn component(mut self, component: impl Component + 'static, row: u8) -> Self { + self.components.push((row, Arc::new(component))); + + self + } + + pub async fn send(self) -> Result, Error> { + let mut builder = self.handlers.http.create_message(self.channel_id); + + if let Some(content) = &self.content { + builder = builder.content(content); + } + + let mut current_row = ActionRow::new(&[]); + let mut current_index = 0; + + let mut rows: Vec = vec![]; + let mut custom_ids: Vec = vec![]; + + for (index, component) in self.components { + if index != current_index { + if current_row.len() > 0 { + rows.push(current_row.build()); + current_row = ActionRow::new(&[]); + } + + current_index += 1; + } + + let custom_id = component.custom_id(); + + if let Some(custom_id) = &custom_id { + custom_ids.push(custom_id.clone()); + + self.handlers + .registry + .register(custom_id, component.clone()); + } + + current_row.add(custom_id.clone(), component.clone()); + } + + if current_row.len() > 0 { + rows.push(current_row.build()); + } + + builder = builder.components(&rows); + builder = builder.embeds(&self.embeds); + + let result = builder.await; + + if result.is_err() { + for custom_id in custom_ids { + self.handlers.registry.unregister(custom_id); + } + } + + result + } +} + +impl IntoFuture for MessageSender { + type Output = Result, Error>; + type IntoFuture = Pin + Send>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(self.send()) + } +} + +pub struct OriginalMessageUpdater { + interaction_id: Id, + interaction_token: String, + + handlers: Arc, + + content: Option, + embeds: Vec, + + // Row index, component + components: Vec<(u8, Arc)>, +} + +impl OriginalMessageUpdater { + pub fn new( + interaction_id: Id, + interaction_token: String, + handlers: Arc, + ) -> Self { + Self { + interaction_id, + interaction_token, + handlers, + content: None, + components: vec![], + embeds: vec![], + } + } + + pub fn content(mut self, content: impl ToString) -> Self { + self.content = Some(content.to_string()); + + self + } + + pub fn embed(mut self, embed: Embed) -> Self { + self.embeds.push(embed); + + self + } + + pub fn component(mut self, component: impl Component + 'static, row: u8) -> Self { + self.components.push((row, Arc::new(component))); + + self + } + + pub async fn send(self) -> Result, Error> { + let mut builder = InteractionResponseDataBuilder::new(); + + if let Some(content) = &self.content { + builder = builder.content(content); + } + + let mut current_row = ActionRow::new(&[]); + let mut current_index = 0; + + let mut rows: Vec = vec![]; + let mut custom_ids: Vec = vec![]; + + for (index, component) in self.components { + if index != current_index { + if current_row.len() > 0 { + rows.push(current_row.build()); + current_row = ActionRow::new(&[]); + } + + current_index += 1; + } + + let custom_id = component.custom_id(); + + if let Some(custom_id) = &custom_id { + custom_ids.push(custom_id.clone()); + + self.handlers + .registry + .register(custom_id, component.clone()); + } + + current_row.add(custom_id.clone(), component.clone()); + } + + if current_row.len() > 0 { + rows.push(current_row.build()); + } + + builder = builder.components(rows); + builder = builder.embeds(self.embeds); + + let result = self + .handlers + .http + .interaction(self.handlers.app.id) + .create_response( + self.interaction_id, + &self.interaction_token, + &InteractionResponse { + data: Some(builder.build()), + kind: InteractionResponseType::UpdateMessage, + }, + ) + .await; + + if result.is_err() { + for custom_id in custom_ids { + self.handlers.registry.unregister(custom_id); + } + } + + result + } +} + +impl IntoFuture for OriginalMessageUpdater { + type Output = Result, Error>; + type IntoFuture = Pin + Send>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(self.send()) + } +} diff --git a/src/framework/commands/mod.rs b/src/framework/commands/mod.rs new file mode 100644 index 0000000..7d4ebd4 --- /dev/null +++ b/src/framework/commands/mod.rs @@ -0,0 +1,69 @@ +use async_trait::async_trait; +use thiserror::Error; + +use crate::framework::commands::{arguments::Argument, context::CommandContext}; + +pub mod arguments; +pub mod context; +pub mod messages; + +#[derive(Debug, Error)] +pub enum CommandError { + #[error("A message was attempted to be sent but it failed")] + MessageSendingFailed, +} + +#[async_trait] +pub trait Command: Send + Sync { + fn name(&self) -> &'static str; + fn aliases(&self) -> &'static [&'static str] { + &[] + } + fn description(&self) -> &'static str { + "A Payphone command." + } + fn arguments(&self) -> &'static [Argument] { + &[] + } + + /// Returns the command name + all its aliases. + /// + /// Returns: + /// [`Vec<&'static str>`] - A vector with the command's name and its aliases. + fn identifiers(&self) -> Vec<&'static str> { + let mut all = vec![self.name()]; + all.extend_from_slice(self.aliases()); + all + } + + async fn execute(&self, ctx: &CommandContext, args: &str) -> Result<(), CommandError>; +} + +pub struct CommandParts<'a> { + pub name: &'a str, + pub args: &'a str, +} + +impl<'a> CommandParts<'a> { + fn new(name: &'a str, args: &'a str) -> Self { + Self { name, args } + } +} + +pub fn get_command_parts<'a>(prefix: &str, message: &'a str) -> Option> { + let message_trimmed_start = message.trim_ascii_start(); + + if message_trimmed_start + .to_ascii_lowercase() + .starts_with(&prefix.to_ascii_lowercase()) + { + let message = message_trimmed_start.split_at(prefix.len()).1; + + match message.split_once(" ") { + Some((name, args)) => Some(CommandParts::new(name, args)), + None => Some(CommandParts::new(message, "")), + } + } else { + None + } +} diff --git a/src/framework/handlers.rs b/src/framework/handlers.rs new file mode 100644 index 0000000..00ee5b7 --- /dev/null +++ b/src/framework/handlers.rs @@ -0,0 +1,12 @@ +use scylla::client::session::Session; +use twilight_http::Client; +use twilight_model::oauth::Application; + +use crate::framework::interactions::registry::Registry; + +pub struct Handlers { + pub http: Client, + pub app: Application, + pub db: Session, + pub registry: Registry, +} diff --git a/src/framework/interactions/components.rs b/src/framework/interactions/components.rs new file mode 100644 index 0000000..138699c --- /dev/null +++ b/src/framework/interactions/components.rs @@ -0,0 +1,103 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use twilight_model::channel::message::{ + Component as TwilightComponent, + component::{ActionRow as TwilightActionRow, Button as TwilightButton, ButtonStyle}, +}; + +use crate::framework::interactions::{InteractionError, context::InteractionContext}; + +pub struct ActionRow { + components: Vec<(Option, Arc)>, +} + +impl ActionRow { + pub fn new(components: &[(Option, Arc)]) -> Self { + Self { + components: components.to_vec(), + } + } + + pub fn add(&mut self, id: Option, component: Arc) { + self.components.push((id, component)); + } + + pub fn len(&self) -> usize { + self.components.len() + } + + pub fn build(self) -> TwilightComponent { + TwilightComponent::ActionRow(TwilightActionRow { + components: self + .components + .into_iter() + .map(|(id, c)| c.build(id)) + .collect(), + }) + } +} + +#[async_trait] +pub trait Component: Send + Sync { + fn custom_id(&self) -> Option { + Some(String::from("component")) + } + fn build(&self, id: Option) -> TwilightComponent; + + async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError>; +} + +#[async_trait] +impl Component for T +where + T: Button, +{ + fn custom_id(&self) -> Option { + Button::id(self) + } + + fn build(&self, id: Option) -> TwilightComponent { + TwilightComponent::Button(TwilightButton { + custom_id: id, + disabled: false, + emoji: None, + label: self.label().map(|i| i.to_string()), + style: if self.url().is_none() { + self.style() + } else { + ButtonStyle::Link + }, + url: self.url().map(|i| i.to_string()), + sku_id: None, + }) + } + + async fn execute(&self, ctx: InteractionContext) -> Result<(), InteractionError> { + self.on_press(ctx).await + } +} + +#[async_trait] +pub trait Button: Send + Sync { + fn id(&self) -> Option { + if self.url().is_none() { + Some(String::from("Button")) + } else { + None + } + } + fn style(&self) -> ButtonStyle { + ButtonStyle::Secondary + } + fn label(&self) -> Option<&'static str> { + None + } + fn url(&self) -> Option<&'static str> { + None + } + + async fn on_press(&self, ctx: InteractionContext) -> Result<(), InteractionError> { + ctx.defer().await + } +} diff --git a/src/framework/interactions/context.rs b/src/framework/interactions/context.rs new file mode 100644 index 0000000..fbebb06 --- /dev/null +++ b/src/framework/interactions/context.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use twilight_http::Client; +use twilight_model::{ + gateway::payload::incoming::InteractionCreate, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::{Id, marker::InteractionMarker}, + oauth::Application, +}; + +use crate::framework::{ + commands::messages::OriginalMessageUpdater, handlers::Handlers, interactions::{registry::Registry, InteractionError} +}; + +pub struct InteractionContext { + pub event: InteractionCreate, + pub handlers: Arc, +} + +impl InteractionContext { + pub fn new( + event: InteractionCreate, + handlers: Arc, + ) -> Self { + Self { + event, + handlers + } + } + + pub async fn defer(&self) -> Result<(), InteractionError> { + self.handlers.http + .interaction(self.handlers.app.id) + .create_response( + self.event.id, + &self.event.token, + &InteractionResponse { + data: None, + kind: InteractionResponseType::DeferredUpdateMessage, + }, + ) + .await + .map_err(|_| InteractionError::DeferringFailed)?; + + Ok(()) + } + + pub async fn think(&self) -> Result<(), InteractionError> { + self.handlers.http + .interaction(self.handlers.app.id) + .create_response( + self.event.id, + &self.event.token, + &InteractionResponse { + data: None, + kind: InteractionResponseType::DeferredChannelMessageWithSource, + }, + ) + .await + .map_err(|_| InteractionError::DeferringFailed)?; + + Ok(()) + } + + pub fn original_message(&self) -> Option { + self.event.message.as_ref().map(|_| OriginalMessage { + interaction_id: self.event.id, + interaction_token: self.event.token.clone(), + handlers: self.handlers.clone() + }) + } +} + +pub struct OriginalMessage { + interaction_id: Id, + interaction_token: String, + handlers: Arc +} + +impl OriginalMessage { + pub fn edit(&self) -> OriginalMessageUpdater { + OriginalMessageUpdater::new( + self.interaction_id, + self.interaction_token.clone(), + self.handlers.clone(), + ) + } +} diff --git a/src/framework/interactions/mod.rs b/src/framework/interactions/mod.rs new file mode 100644 index 0000000..4977032 --- /dev/null +++ b/src/framework/interactions/mod.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +pub mod components; +pub mod context; +pub mod registry; + +#[derive(Debug, Error)] +pub enum InteractionError { + #[error( + "The ID for this interaction did not point to any existing interaction in the registry" + )] + InteractionNotFound, + + #[error("The interaction was attempted to be deferred, but it failed to be done")] + DeferringFailed, + + #[error("Discord responded with an interaction type Payphone does not use")] + InteractionTypeUnsent, + + #[error("The original message was attempted to be edited, but it failed")] + OriginalMessageEditingFailed, +} diff --git a/src/framework/interactions/registry.rs b/src/framework/interactions/registry.rs new file mode 100644 index 0000000..dd44b5c --- /dev/null +++ b/src/framework/interactions/registry.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use scc::HashMap; +use twilight_http::Client; +use twilight_model::{ + application::interaction::InteractionData, gateway::payload::incoming::InteractionCreate, + oauth::Application, +}; + +use crate::framework::{handlers::Handlers, interactions::{context::InteractionContext, InteractionError}}; + +use super::components::Component; + +pub struct Registry { + inner: HashMap>, +} + +impl Registry { + pub fn new() -> Self { + Self { + inner: Default::default(), + } + } + + pub fn register(&self, id: impl ToString, component: Arc) { + self.inner.upsert(id.to_string(), component); + } + + pub fn unregister(&self, id: impl ToString) { + self.inner.remove(&id.to_string()); + } + + pub async fn execute( + &self, + event: InteractionCreate, + handlers: Arc, + ) -> Result<(), InteractionError> { + if let Some(data) = &event.data { + match data { + InteractionData::MessageComponent(data) => { + if let Some(component) = self.inner.get_async(&data.custom_id).await { + component + .execute(InteractionContext::new(event, handlers)) + .await + } else { + Err(InteractionError::InteractionNotFound) + } + } + _ => Err(InteractionError::InteractionTypeUnsent), + } + } else { + Err(InteractionError::InteractionTypeUnsent) + } + } +} diff --git a/src/framework/mod.rs b/src/framework/mod.rs new file mode 100644 index 0000000..fdabf55 --- /dev/null +++ b/src/framework/mod.rs @@ -0,0 +1,4 @@ +pub mod commands; +pub mod interactions; +pub mod utils; +pub mod handlers; diff --git a/src/framework/utils.rs b/src/framework/utils.rs new file mode 100644 index 0000000..8bfd7ad --- /dev/null +++ b/src/framework/utils.rs @@ -0,0 +1,33 @@ +pub trait EscapeDiscordMarkdown { + fn escape(self) -> String; +} + +impl EscapeDiscordMarkdown for T +where + T: ToString, +{ + fn escape(self) -> String { + let input = self.to_string(); + + let mut escaped = String::with_capacity(input.len()); + + for ch in input.chars() { + match ch { + '\\' | '*' | '_' | '~' | '`' | '>' | '|' | '[' | ']' | '(' | ')' | '#' | '+' + | '-' | '=' | '.' | '!' => { + escaped.push('\\'); + escaped.push(ch); + } + _ => escaped.push(ch), + } + } + + // Now block ping attempts — break mentions + escaped + .replace("@everyone", "@\u{200B}everyone") + .replace("@here", "@\u{200B}here") + .replace("<@", "<@\u{200B}") // user mentions + .replace("<@&", "<@&\u{200B}") // role mentions + .replace("<#", "<#\u{200B}") // channel mentions + } +} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..13600b5 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,57 @@ +use colored::Colorize; +use log::{Level, LevelFilter}; +use time::{OffsetDateTime, macros::format_description}; + +pub struct Logger { + level: Level, +} + +impl log::Log for Logger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + println!( + "{} {} {}", + get_formatted_timestamp(), + get_formatted_level(record.level()), + record.args(), + ); + } + } + + fn flush(&self) {} +} + +static LOGGER: Logger = Logger { level: Level::Info }; + +pub fn init() { + log::set_logger(&LOGGER) + .map(|()| log::set_max_level(LevelFilter::Trace)) + .unwrap(); +} + +fn get_formatted_timestamp() -> String { + let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc()); + + let format = format_description!( + "[day]/[month]/[year] at [hour]:[minute]:[second].[subsecond digits:2]" + ); + + now.format(&format).unwrap().dimmed().to_string() +} + +fn get_formatted_level(level: Level) -> String { + let string = format!("[{level}]"); + let string = format!("{string:<7}"); + + match level { + Level::Trace => string.dimmed().to_string(), + Level::Debug => string.white().to_string(), + Level::Info => string.blue().to_string(), + Level::Warn => string.yellow().to_string(), + Level::Error => string.red().bold().to_string(), + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..aa5222b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use clap::Parser; +use twilight_gateway::{ConfigBuilder, EventTypeFlags, Intents, Shard, ShardId, StreamExt}; +use twilight_http::Client; +use twilight_model::gateway::{ + payload::outgoing::update_presence::UpdatePresencePayload, + presence::{ActivityType, MinimalActivity, Status}, +}; + +use crate::{ + cli::Args, commands::register_default_components, discord::events, + framework::{handlers::Handlers, interactions::registry::Registry}, +}; + +mod cli; +mod commands; +mod constants; +mod database; +mod discord; +mod framework; +mod logging; +mod utils; + +#[tokio::main] +async fn main() { + dotenvy::dotenv().ok(); + + let args = Args::parse(); + + logging::init(); + + let intents = Intents::GUILD_MESSAGES | Intents::MESSAGE_CONTENT; + let config = ConfigBuilder::new(args.token.clone(), intents) + .presence( + UpdatePresencePayload::new( + vec![ + MinimalActivity { + kind: ActivityType::Listening, + name: "Hello Payphone!".into(), + url: None, + } + .into(), + ], + false, + None, + Status::Online, + ) + .expect("Could not set presence!"), + ) + .build(); + let mut shard = Shard::with_config(ShardId::new(0, 1), config); + + let http = Client::new(args.token.clone()); + let registry = Registry::new(); + let app = + http.current_user_application() + .await + .expect("Could not get information about the current application") + .model() + .await + .expect("Could not get model of the information of the current application") + ; + + register_default_components(®istry); + + log::info!("Welcome to Payphone, Nyeki!"); + log::info!( + "Payphone is running on version v{}", + env!("CARGO_PKG_VERSION") + ); + + let db = database::init("127.0.0.1:9042") + .await + .expect("Could not connect to the database!"); + + let handlers = Arc::new(Handlers { + app, + db, + http, + registry + }); + + while let Some(event) = shard.next_event(EventTypeFlags::all()).await { + if let Ok(event) = event { + tokio::spawn(events::handle_event( + event, + handlers.clone(), + )); + } else { + log::error!("Could not receive the next event."); + } + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..a0d8e6a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +pub trait ToTitleCase { + fn to_title_case(&self) -> String; +} + +impl ToTitleCase for T +where + T: ToString, +{ + fn to_title_case(&self) -> String { + self.to_string() + .split_whitespace() + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }) + .collect::>() + .join(" ") + } +}