diff --git a/.forgejo/workflows/check_lfs.yaml b/.forgejo/workflows/check_lfs.yaml new file mode 100644 index 0000000..aaa2117 --- /dev/null +++ b/.forgejo/workflows/check_lfs.yaml @@ -0,0 +1,10 @@ +on: + - push + - workflow_dispatch + +jobs: + check_lfs: + # nix/docker + runs-on: nix + steps: + - uses: https://github.com/MPLew-is/lfs-check-action@1 \ No newline at end of file diff --git a/.forgejo/workflows/on_pr.yaml b/.forgejo/workflows/on_pr.yaml new file mode 100644 index 0000000..d17dc47 --- /dev/null +++ b/.forgejo/workflows/on_pr.yaml @@ -0,0 +1,51 @@ +on: + - pull_request + +jobs: + check_lfs: + # nix/docker + runs-on: nix + steps: + - uses: https://github.com/MPLew-is/lfs-check-action@1 + + # rust code must be formatted for standardisation + lint_fmt: + # build it using teh base nixos system, helps with caching + runs-on: nix + steps: + # get the repo first + - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs/nix@v8 + with: + server_url: ${{ gitea.server_url }} + repository: ${{ gitea.repository }} + - run: nix build .#fmt --verbose + + # clippy is incredibly useful for making yer code better + lint_clippy: + # build it using teh base nixos system, helps with caching + runs-on: nix + permissions: + checks: write + steps: + # get the repo first + - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs/nix@v8 + with: + server_url: ${{ gitea.server_url }} + repository: ${{ gitea.repository }} + - run: nix build .#clippy --verbose + + build: + # build it using teh base nixos system, helps with caching + runs-on: nix + needs: [ lint_fmt, lint_clippy ] + steps: + # get the repo first + - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs/nix@v8 + with: + server_url: ${{ gitea.server_url }} + repository: ${{ gitea.repository }} + - name: "Build it locally" + run: nix build --verbose diff --git a/.forgejo/workflows/push.yaml b/.forgejo/workflows/push.yaml index 28f49f6..984c5d6 100644 --- a/.forgejo/workflows/push.yaml +++ b/.forgejo/workflows/push.yaml @@ -19,6 +19,10 @@ jobs: steps: # get the repo first - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3 + with: + repository: ${{ gitea.repository }} + ref_name: ${{ gitea.ref_name }} - run: nix build .#fmt --verbose # clippy is incredibly useful for making yer code better @@ -30,6 +34,10 @@ jobs: steps: # get the repo first - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3 + with: + repository: ${{ gitea.repository }} + ref_name: ${{ gitea.ref_name }} - run: nix build .#clippy --verbose build: @@ -39,6 +47,10 @@ jobs: steps: # get the repo first - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3 + with: + repository: ${{ gitea.repository }} + ref_name: ${{ gitea.ref_name }} - name: "Build it locally" run: nix build --verbose @@ -49,7 +61,7 @@ jobs: needs: [ build ] steps: - name: "Deploy to Skynet" - uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2 + uses: https://forgejo.skynet.ie/Skynet/actions/deploy@v3 with: input: 'skynet_discord_bot' token: ${{ secrets.API_TOKEN_FORGEJO }} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6c7adef --- /dev/null +++ b/.gitattributes @@ -0,0 +1,37 @@ +# Git config here +* text eol=lf + +############################################# +# Git lfs stuff +# Documents +*.pdf filter=lfs diff=lfs merge=lfs -text +*.doc filter=lfs diff=lfs merge=lfs -text +*.docx filter=lfs diff=lfs merge=lfs -text +# Excel +*.xls filter=lfs diff=lfs merge=lfs -text +*.xlsx filter=lfs diff=lfs merge=lfs -text +*.xlsm filter=lfs diff=lfs merge=lfs -text +# Powerpoints +*.ppt filter=lfs diff=lfs merge=lfs -text +*.pptx filter=lfs diff=lfs merge=lfs -text +*.ppsx filter=lfs diff=lfs merge=lfs -text +# Images +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +# Video +*.mkv filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.wmv filter=lfs diff=lfs merge=lfs -text +# Misc +*.zip filter=lfs diff=lfs merge=lfs -text +# ET4011 +*.cbe filter=lfs diff=lfs merge=lfs -text +*.pbs filter=lfs diff=lfs merge=lfs -text +# Open/Libre office +# from https://www.libreoffice.org/discover/what-is-opendocument/ +*.odt filter=lfs diff=lfs merge=lfs -text +*.ods filter=lfs diff=lfs merge=lfs -text +*.odp filter=lfs diff=lfs merge=lfs -text +*.odg filter=lfs diff=lfs merge=lfs -text +# QT +*.ui filter=lfs diff=lfs merge=lfs -text diff --git a/.rustfmt.toml b/.rustfmt.toml index b8ae8dd..2b0831a 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -6,4 +6,5 @@ fn_params_layout = "Compressed" #brace_style = "PreferSameLine" struct_lit_width = 0 tab_spaces = 2 -use_small_heuristics = "Max" \ No newline at end of file +use_small_heuristics = "Max" +imports_granularity = "Crate" \ No newline at end of file diff --git a/.server-icons.toml b/.server-icons.toml new file mode 100644 index 0000000..eeb302c --- /dev/null +++ b/.server-icons.toml @@ -0,0 +1,6 @@ +# this file controls the + +[source] +repo = "https://forgejo.skynet.ie/Computer_Society/open-goverance" +directory = "Resources/Logo_Variants" +file = "_festivals.toml" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d8c9113..66e67f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -78,10 +78,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.15", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -107,9 +106,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "async-channel" @@ -143,7 +157,7 @@ dependencies = [ "async-task", "concurrent-queue", "fastrand 2.1.1", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "slab", ] @@ -158,21 +172,21 @@ dependencies = [ "async-io", "async-lock", "blocking", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "once_cell", ] [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "parking", "polling", "rustix", @@ -206,7 +220,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -218,6 +232,28 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "async-task" version = "4.7.1" @@ -232,23 +268,7 @@ checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", -] - -[[package]] -name = "async-tungstenite" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b71b31561643aa8e7df3effe284fa83ab1a840e52294c5f4bd7bfd8b2becbb" -dependencies = [ - "futures-io", - "futures-util", - "log", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tungstenite", - "webpki-roots 0.22.6", + "syn 2.0.89", ] [[package]] @@ -359,7 +379,7 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite 2.5.0", "piper", ] @@ -369,6 +389,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" + [[package]] name = "byteorder" version = "1.5.0" @@ -389,9 +415,9 @@ checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.1.19" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ "shlex", ] @@ -404,9 +430,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -416,6 +442,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "cipher" version = "0.2.5" @@ -425,6 +461,39 @@ dependencies = [ "generic-array", ] +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -564,24 +633,24 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.46" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" +checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2 0.5.7", + "socket2", "windows-sys 0.52.0", ] [[package]] name = "curl-sys" -version = "0.4.75+curl-8.10.0" +version = "0.4.78+curl-8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4fd752d337342e4314717c0d9b6586b059a120c80029ebe4d49b11fec7875e" +checksum = "8eec768341c5c7789611ae51cf6c459099f22e64a5d5d0ce4892434e33821eaf" dependencies = [ "cc", "libc", @@ -600,13 +669,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", "serde", ] +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "data-url" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" + [[package]] name = "der" version = "0.7.9" @@ -655,6 +736,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -672,9 +764,9 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87260449b06739ee78d6281c68d2a0ff3e3af64a78df63d3a1aeb3c06997c8a" +checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" dependencies = [ "base64 0.22.1", "memchr", @@ -749,6 +841,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -764,6 +866,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "flate2" version = "1.0.33" @@ -774,6 +885,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + [[package]] name = "flume" version = "0.9.2" @@ -793,7 +910,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -802,6 +919,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff20bef7942a72af07104346154a70a70b089c572e454b41bef6eb6cb10e9c06" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "ttf-parser", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -901,9 +1045,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand 2.1.1", "futures-core", @@ -920,7 +1064,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -953,6 +1097,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -985,6 +1138,18 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + [[package]] name = "ghash" version = "0.3.1" @@ -995,6 +1160,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.0" @@ -1024,7 +1199,26 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes 1.7.1", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -1043,22 +1237,30 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.8.4" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "hashbrown", + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.2", ] [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -1127,13 +1329,13 @@ dependencies = [ [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows", ] [[package]] @@ -1147,6 +1349,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes 1.7.1", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -1154,7 +1367,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes 1.7.1", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes 1.7.1", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes 1.7.1", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1216,20 +1452,40 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes 1.7.1", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-rustls" version = "0.24.2" @@ -1237,13 +1493,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.12", + "hyper 0.14.30", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.1", + "hyper-util", + "rustls 0.23.18", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.7.1", + "http-body-util", + "hyper 1.5.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes 1.7.1", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1268,25 +1576,156 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.3.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", ] +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imagesize" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.5.0" @@ -1294,7 +1733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1330,7 +1769,7 @@ dependencies = [ "curl-sys", "flume 0.9.2", "futures-lite 1.13.0", - "http", + "http 0.2.12", "log", "once_cell", "slab", @@ -1347,6 +1786,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.70" @@ -1356,6 +1801,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kurbo" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a53776d271cfb873b17c618af0298445c88afc52837f3e948fa3fafd131f449" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "kurbo" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd85a5776cd9500c2e2059c8c76c3b01528566b7fcbaf8098b55a33fc298849b" +dependencies = [ + "arrayvec", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1371,30 +1834,32 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.9.8", + "spin", ] [[package]] name = "lettre" -version = "0.10.4" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" +checksum = "504273f23d9f3d2fd09c6e5fa94fafd5177ae6b83ed0df1f3b0e180052c076a9" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", + "chumsky", "email-encoding", "email_address", - "fastrand 1.9.0", + "fastrand 2.1.1", "futures-util", "hostname", "httpdate", - "idna 0.3.0", + "idna", "mime", "native-tls", "nom", - "once_cell", + "percent-encoding", "quoted_printable", - "socket2 0.4.10", + "socket2", "tokio", + "url", ] [[package]] @@ -1421,9 +1886,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.27.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "cc", "pkg-config", @@ -1448,6 +1913,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -1467,17 +1938,11 @@ dependencies = [ "value-bag", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "maud" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" dependencies = [ "itoa", "maud_macros", @@ -1485,14 +1950,14 @@ dependencies = [ [[package]] name = "maud_macros" -version = "0.25.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be95d66c3024ffce639216058e5bae17a83ecaf266ffc6e4d060ad447c9eed2" +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" dependencies = [ - "proc-macro-error", "proc-macro2", + "proc-macro2-diagnostics", "quote", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] @@ -1511,6 +1976,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -1540,6 +2014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1678,7 +2153,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1700,13 +2175,10 @@ dependencies = [ ] [[package]] -name = "ordered-float" -version = "2.10.1" +name = "owo-colors" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" [[package]] name = "parking" @@ -1737,12 +2209,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1759,23 +2225,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pin-project" -version = "1.1.5" +name = "pico-args" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -1829,10 +2301,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "polling" -version = "3.7.3" +name = "png" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -1866,31 +2351,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "zerocopy 0.7.35", ] [[package]] @@ -1901,13 +2362,34 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "version_check", +] + +[[package]] +name = "psm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.37" @@ -1919,9 +2401,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.8" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" [[package]] name = "rand" @@ -1947,6 +2429,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.1", + "zerocopy 0.8.18", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -1967,6 +2460,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[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 0.9.1", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -1985,6 +2488,16 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.18", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1994,6 +2507,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rctree" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f" + [[package]] name = "redox_syscall" version = "0.5.4" @@ -2014,11 +2533,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", @@ -2028,12 +2547,12 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls 0.21.12", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", - "system-configuration", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -2048,18 +2567,74 @@ dependencies = [ ] [[package]] -name = "ring" -version = "0.16.20" +name = "reqwest" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ - "cc", - "libc", + "base64 0.22.1", + "bytes 1.7.1", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.1", + "hyper-rustls 0.27.3", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", - "winapi", + "windows-registry", +] + +[[package]] +name = "resvg" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76888219c0881e22b0ceab06fddcfe83163cd81642bd60c7842387f9c968a72e" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "png", + "rgb", + "svgfilters", + "svgtypes 0.10.0", + "tiny-skia", + "usvg", + "usvg-text-layout", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", ] [[package]] @@ -2072,11 +2647,39 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin 0.9.8", - "untrusted 0.9.0", + "spin", + "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rosvgtree" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc23d1ace03d6b8153c7d16f0708cd80b61ee8e80304954803354e67e40d150" +dependencies = [ + "log", + "roxmltree 0.18.1", + "simplecss", + "siphasher", + "svgtypes 0.9.0", +] + +[[package]] +name = "roxmltree" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + [[package]] name = "rsa" version = "0.9.6" @@ -2125,18 +2728,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.12" @@ -2144,11 +2735,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.8", - "rustls-webpki", + "ring", + "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -2158,14 +2776,56 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustybuzz" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162bdf42e261bee271b3957691018634488084ef577dddeb6420a9684cab2a6a" +dependencies = [ + "bitflags 1.3.2", + "bytemuck", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-general-category", + "unicode-script", ] [[package]] @@ -2195,8 +2855,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", ] [[package]] @@ -2239,39 +2909,38 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] -name = "serde-value" -version = "0.7.0" +name = "serde_cow" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" dependencies = [ - "ordered-float", "serde", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2287,7 +2956,16 @@ checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" dependencies = [ "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.63", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", ] [[package]] @@ -2304,45 +2982,35 @@ dependencies = [ [[package]] name = "serenity" -version = "0.11.7" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7a89cef23483fc9d4caf2df41e6d3928e18aada84c56abd237439d929622c6" +checksum = "3d72ec4323681bf9a3cabe40fd080abc2435859b502a1b5aa9bf693f125bfa76" dependencies = [ + "arrayvec", "async-trait", - "async-tungstenite", - "base64 0.21.7", - "bitflags 1.3.2", + "base64 0.22.1", + "bitflags 2.6.0", "bytes 1.7.1", - "cfg-if", "dashmap", "flate2", "futures", - "mime", + "fxhash", "mime_guess", "parking_lot", "percent-encoding", - "reqwest", + "reqwest 0.11.27", + "secrecy", "serde", - "serde-value", + "serde_cow", "serde_json", "time 0.3.36", "tokio", + "tokio-tungstenite", "tracing", "typemap_rev", "url", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.6.1" @@ -2393,12 +3061,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2409,20 +3095,50 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "skynet_discord_bot" version = "0.1.0" dependencies = [ "chrono", + "color-eyre", "dotenvy", + "eyre", "lettre", "maud", - "rand 0.8.5", + "rand 0.9.0", + "resvg", "serde", + "serde_json", "serenity", "sqlx", "surf", + "tiny-skia", "tokio", + "toml", + "usvg", + "usvg-text-layout", + "wolves_oxidised", ] [[package]] @@ -2450,15 +3166,8 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ - "libc", - "winapi", + "serde", ] [[package]] @@ -2471,12 +3180,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -2505,21 +3208,11 @@ dependencies = [ "der", ] -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom", - "unicode_categories", -] - [[package]] name = "sqlx" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2530,37 +3223,31 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ - "ahash", - "atoi", - "byteorder", "bytes 1.7.1", "crc", "crossbeam-queue", "either", - "event-listener 2.5.3", - "futures-channel", + "event-listener 5.3.1", "futures-core", "futures-intrusive", "futures-io", "futures-util", + "hashbrown 0.15.2", "hashlink", - "hex", "indexmap", "log", "memchr", "once_cell", - "paste", "percent-encoding", "serde", "serde_json", "sha2 0.10.8", "smallvec", - "sqlformat", - "thiserror", + "thiserror 2.0.11", "tokio", "tokio-stream", "tracing", @@ -2569,22 +3256,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 1.0.109", + "syn 2.0.89", ] [[package]] name = "sqlx-macros-core" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" dependencies = [ "dotenvy", "either", @@ -2598,8 +3285,9 @@ dependencies = [ "sha2 0.10.8", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", - "syn 1.0.109", + "syn 2.0.89", "tempfile", "tokio", "url", @@ -2607,12 +3295,12 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.6.0", "byteorder", "bytes 1.7.1", @@ -2642,19 +3330,19 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.11", "tracing", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", - "base64 0.21.7", + "base64 0.22.1", "bitflags 2.6.0", "byteorder", "crc", @@ -2662,7 +3350,6 @@ dependencies = [ "etcetera", "futures-channel", "futures-core", - "futures-io", "futures-util", "hex", "hkdf 0.12.4", @@ -2680,16 +3367,16 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.11", "tracing", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.7.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", "flume 0.11.0", @@ -2702,10 +3389,29 @@ dependencies = [ "log", "percent-encoding", "serde", + "serde_urlencoded", "sqlx-core", "tracing", "url", - "urlencoding", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stacker" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d08feb8f695b465baed819b03c128dc23f57a694510ab1f06c77f763975685e" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", ] [[package]] @@ -2766,6 +3472,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -2806,6 +3521,36 @@ dependencies = [ "web-sys", ] +[[package]] +name = "svgfilters" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "639abcebc15fdc2df179f37d6f5463d660c1c79cd552c12343a4600827a04bce" +dependencies = [ + "float-cmp", + "rgb", +] + +[[package]] +name = "svgtypes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ee29c1407a5b18ccfe5f6ac82ac11bab3b14407e09c209a6c1a32098b19734" +dependencies = [ + "kurbo 0.8.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ffacedcdcf1da6579c907279b4f3c5492fbce99fbbf227f5ed270a589c2765" +dependencies = [ + "kurbo 0.9.5", + "siphasher", +] + [[package]] name = "syn" version = "1.0.109" @@ -2819,9 +3564,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -2834,6 +3579,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -2842,7 +3607,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -2855,6 +3631,16 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.12.0" @@ -2874,7 +3660,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -2885,7 +3680,28 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -2957,6 +3773,41 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tiny-skia" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2974,16 +3825,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes 1.7.1", "libc", "mio", + "parking_lot", "pin-project-lite", - "socket2 0.5.7", + "signal-hook-registry", + "socket2", "tokio-macros", "windows-sys 0.52.0", ] @@ -2996,18 +3849,17 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] -name = "tokio-rustls" -version = "0.23.4" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "rustls 0.20.9", + "native-tls", "tokio", - "webpki", ] [[package]] @@ -3020,6 +3872,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.18", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.16" @@ -3031,6 +3905,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes 1.7.1", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite", + "webpki-roots 0.26.8", +] + [[package]] name = "tokio-util" version = "0.7.12" @@ -3044,6 +3947,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower-service" version = "0.3.3" @@ -3070,7 +4014,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", ] [[package]] @@ -3080,6 +4024,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", ] [[package]] @@ -3092,6 +4047,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -3099,31 +4065,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "tungstenite" -version = "0.17.3" +name = "ttf-parser" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ - "base64 0.13.1", "byteorder", "bytes 1.7.1", - "http", + "data-encoding", + "http 1.1.0", "httparse", "log", "rand 0.8.5", - "rustls 0.20.9", - "sha-1", - "thiserror", + "rustls 0.22.4", + "rustls-pki-types", + "sha1 0.10.6", + "thiserror 1.0.63", "url", "utf-8", - "webpki", ] [[package]] name = "typemap_rev" -version = "0.1.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5b74f0a24b5454580a79abb6994393b09adf0ab8070f15827cb666255de155" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" [[package]] name = "typenum" @@ -3146,6 +4118,24 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +[[package]] +name = "unicode-bidi-mirroring" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694" + +[[package]] +name = "unicode-ccc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" + +[[package]] +name = "unicode-general-category" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -3168,16 +4158,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "unicode-script" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] -name = "unicode_categories" -version = "0.1.1" +name = "unicode-vo" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "universal-hash" @@ -3189,12 +4179,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -3203,21 +4187,48 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna", "percent-encoding", "serde", ] [[package]] -name = "urlencoding" -version = "2.1.3" +name = "usvg" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +checksum = "63b6bb4e62619d9f68aa2d8a823fea2bff302340a1f2d45c264d5b0be170832e" +dependencies = [ + "base64 0.21.7", + "data-url", + "flate2", + "imagesize", + "kurbo 0.9.5", + "log", + "rctree", + "rosvgtree", + "strict-num", +] + +[[package]] +name = "usvg-text-layout" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "195386e01bc35f860db024de275a76e7a31afdf975d18beb6d0e44764118b4db" +dependencies = [ + "fontdb", + "kurbo 0.9.5", + "log", + "rustybuzz", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "usvg", +] [[package]] name = "utf-8" @@ -3226,10 +4237,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "value-bag" -version = "1.9.0" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" [[package]] name = "vcpkg" @@ -3270,6 +4299,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -3298,7 +4336,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -3332,7 +4370,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3366,31 +4404,27 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "webpki-roots" version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + [[package]] name = "whoami" version = "1.5.2" @@ -3423,6 +4457,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -3432,6 +4476,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3580,6 +4654,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -3590,6 +4673,68 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "wolves_oxidised" +version = "0.1.0" +source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#265c8c81d1eb870a6149da5ce72556d44f57937f" +dependencies = [ + "reqwest 0.12.9", + "serde", + "serde_json", + "tokio-test", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -3597,7 +4742,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2" +dependencies = [ + "zerocopy-derive 0.8.18", ] [[package]] @@ -3608,7 +4762,39 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.89", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "synstructure", ] [[package]] @@ -3616,3 +4802,25 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] diff --git a/Cargo.toml b/Cargo.toml index a26023a..832eb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,37 +4,56 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [[bin]] name = "update_data" [[bin]] -name = "update_users" +name = "update_committee" [[bin]] name = "update_minecraft" +[[bin]] +name = "update_server-icon" + +[[bin]] +name = "cleanup_committee" + [dependencies] # discord library -serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } -tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "full"] } + +# wolves api +wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"] } +# wolves_oxidised = { path = "../wolves-oxidised", features = ["unstable"] } # to make the http requests -surf = "2.3.2" +surf = "2.3" -dotenvy = "0.15.7" +dotenvy = "0.15" # For sqlite -sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] } +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "migrate"] } +serde_json = { version = "1.0", features = ["raw_value"] } # create random strings -rand = "0.8.5" +rand = "0.9" # fancy time stuff -chrono = "0.4.26" +chrono = "0.4" # for email -lettre = "0.10.4" -maud = "0.25.0" +lettre = "0.11" +maud = "0.27" -serde = "1.0.188" \ No newline at end of file +toml = "0.8.23" +serde = "1.0" + +# for image conversion +eyre = "0.6.8" +color-eyre = "0.6.2" +usvg-text-layout = "0.29.0" +usvg = "0.29.0" +resvg = "0.29.0" +tiny-skia = "0.8.3" \ No newline at end of file diff --git a/README.md b/README.md index 068979a..1f49dc1 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,10 @@ # Skynet Discord Bot -This bots core purpose is to give members roles based on their status on . -It uses an api key provided by wolves to get member lists. +The Skynet bot is designed to manage users on Discord. +It allows users to link their UL Wolves account with Wolves in a GDPR compliant manner. +Skynet (bot) is hosted by the Computer Society on Skynet (computer cluster). -Users are able to link their wolves account to the bot and that works across discord servers. -For example is a user links on the CompSoc Discord then they will also get their roles (automagically) on Games Dev if they are a member there. +## Documentation +We have split up the documentation into different segments depending on who the user is. -## Commands - Admin - -You need admin access to run any of the commands in this section. -Either the server owner or a suer with the ``Administrator`` permission - -### Getting the Skynet Discord bot -1. Email ``keith@assurememberships.com`` from committee email and say ye want an api key for ``193.1.99.74`` -2. Create a role for current members (maybe call it ``current-member`` ?) -3. (Optional) create a role for all past and current members (ye can use the existing ``member`` role for this, ) -4. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot -5. Make sure the bot role ``@skynet`` is above these two roles (so it can manage them) -6. Make sure that you have a role that gives ye administrator powers -7. Use the command ``/add`` and insert the api key, role current and role all (desktop recommended) - -The reason for both roles is ye have one for active members while the second is for all current and past members. - -### Minecraft -The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society. -Talk to us to get a server. - -#### Add -This links a minecraft server with your club/society. - -``/minecraft_add SERVER_ID`` - - -#### List -List the servers linked to your club/society. -It is possible to have more than one minecraft server - -``/minecraft_list`` - -#### Delete -This unlinks a minecraft server from your club/society. - -``/minecraft_delete SERVER_ID`` - -## Commands - User - -### Setup -* Start the process using ``/link_wolves WOLVES_EMAIL`` - * The email that is in the Contact Email here: -* An email will be sent to them that they need to verify using ``/verify CODE`` -* This will only have to be done once. - -* If the user is an active member on wolves - * If they are in any servers with teh Skynet Bot - * They will get relevant roles. - * If they Join a server with teh bot enabled. - * They will be granted the roles automatically -* If the user is **not** an active member on wolves. - * If they have no Roles - * No change - * If they have Past Member Role - * No change - * If they have both Roles - * The current-member role will be removed from them - * Past Member role will remain unchanged - -### Minecraft -Users can link their Minecraft username to grant them access to any servers where teh whitelist is managed by teh bot. - -``/link_minecraft MINECRAFT_USERNAME`` +* [Committees](./doc/Committee.md) +* [Member](./doc/User.md) \ No newline at end of file diff --git a/db/migrations/10_member_committee-roles.sql b/db/migrations/10_member_committee-roles.sql new file mode 100644 index 0000000..03264cd --- /dev/null +++ b/db/migrations/10_member_committee-roles.sql @@ -0,0 +1,10 @@ + +CREATE TABLE IF NOT EXISTS committee_roles ( + id_wolves integer PRIMARY KEY, + id_role integer DEFAULT 1, + id_channel integer DEFAULT 1, + -- not strictly required but for readability and debugging + name_role text NOT NULL DEFAULT '', + name_channel text NOT NULL DEFAULT '', + count integer DEFAULT 0 +); diff --git a/db/migrations/11_server-icons.sql b/db/migrations/11_server-icons.sql new file mode 100644 index 0000000..20fb472 --- /dev/null +++ b/db/migrations/11_server-icons.sql @@ -0,0 +1,9 @@ + +CREATE TABLE IF NOT EXISTS server_icons ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + path TEXT NOT NULL, + date TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS index_name ON server_icons (name); \ No newline at end of file diff --git a/db/migrations/6_role-adder.sql b/db/migrations/6_role-adder.sql new file mode 100644 index 0000000..7206eca --- /dev/null +++ b/db/migrations/6_role-adder.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS roles_adder ( + server integer not null, + role_a integer not null, + role_b integer not null, + role_c integer not null, + PRIMARY KEY(server,role_a,role_b,role_c) +); +CREATE INDEX IF NOT EXISTS index_roles_adder_server ON roles_adder (server); +CREATE INDEX IF NOT EXISTS index_roles_adder_from ON roles_adder (role_a,role_b); +CREATE INDEX IF NOT EXISTS index_roles_adder_to ON roles_adder (role_c); +CREATE INDEX IF NOT EXISTS index_roles_adder_search ON roles_adder (server,role_a,role_b); \ No newline at end of file diff --git a/db/migrations/7_minecraft-bedrock.sql b/db/migrations/7_minecraft-bedrock.sql new file mode 100644 index 0000000..71d8976 --- /dev/null +++ b/db/migrations/7_minecraft-bedrock.sql @@ -0,0 +1,3 @@ + +-- Using this col we will be able to see if someone has a bedrock account (as well as a java one) +ALTER TABLE wolves ADD COLUMN minecraft_uid TEXT; \ No newline at end of file diff --git a/db/migrations/8_committee-mk-ii.sql b/db/migrations/8_committee-mk-ii.sql new file mode 100644 index 0000000..3c942cd --- /dev/null +++ b/db/migrations/8_committee-mk-ii.sql @@ -0,0 +1,14 @@ +-- No longer using the previous committee table +DROP TABLE committee; + +-- new table pulling from teh api +CREATE TABLE IF NOT EXISTS committees ( + id integer PRIMARY KEY, + name_profile text not null, + name_plain text not null, + name_full text not null, + link text not null, + committee text not null +); + +ALTER TABLE servers DROP COLUMN wolves_link; \ No newline at end of file diff --git a/db/migrations/9_member_committee-id.sql b/db/migrations/9_member_committee-id.sql new file mode 100644 index 0000000..77552ce --- /dev/null +++ b/db/migrations/9_member_committee-id.sql @@ -0,0 +1,6 @@ +-- No need for this col since it is goign to be in "committees" anyways +ALTER TABLE servers DROP COLUMN server_name; + +-- we do care about teh ID of the club/soc though +ALTER TABLE servers ADD COLUMN wolves_id integer DEFAULT 0; + diff --git a/doc/Committee.md b/doc/Committee.md new file mode 100644 index 0000000..a400536 --- /dev/null +++ b/doc/Committee.md @@ -0,0 +1,71 @@ +# Skynet Discord Bot +This bots core purpose is to give members roles based on their status on . +It uses an api key provided by wolves to get member lists. + +Users are able to link their wolves account to the bot and that works across discord servers. +For example is a user links on the CompSoc Discord then they will also get their roles (automagically) on Games Dev if they are a member there. + +## Setup - Committee +You need admin access to run any of the commands in this section. +Either the server owner or a user with the ``Manage Server`` permission. + +### Get the API Key +The ``api_key`` is used by the Bot in order to request information, it will be used later in the process. + +1. Email ``keith@assurememberships.com`` from committee email and say you want an ``api_key`` for ``193.1.99.74`` + * The committee email is the one here: + * This may take up to a week to get the key. + +### Setup Server +The Bot reason for existing is being able to give members Roles. +So we have to create those. + +1. Create a role for Current Members. + * You can call it whatever you want. + * ``member-current`` is a good choice. + * This should be a new role +2. **Optional**: you can create a role that is given to folks who were but no longer a member. + * ``member`` would be a good choice for this + * If you have an existing member role this is also a good fit. + +The reason for both roles is ye have one for active members while the second is for all current and past members. + +### Invite Bot +1. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot +2. Make sure the bot role ``@skynet`` is above these two roles created in the previous step + * This is so it can manage the roles (give and remove them from users) + +### Setup Bot +This is where the bot is configured. +You will need the ``api_key`` from the start of the process. +You (personally) will need a role with ``Manage Server`` permission to be able to do this. + +1. Use the command ``/committee add`` and a list of options will pop up. +2. ``api_key`` is the key you got from Keith earlier. +3. ``role_current`` is the ``member-current`` that you created earlier. +4. ``role_past`` (optional) is the role for all current and past members. +5. ``bot_channel`` is a channel that folks are recommended to use the bot. + * You can have it so folks cannot see message history + +At this point the bot is set up and no further action is required. + +### Minecraft +The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society. +Talk to us to get a server. + +#### Add +This links a minecraft server with your club/society. + +``/minecraft_add SERVER_ID`` + + +#### List +List the servers linked to your club/society. +It is possible to have more than one minecraft server + +``/minecraft_list`` + +#### Delete +This unlinks a minecraft server from your club/society. + +``/minecraft_delete SERVER_ID`` diff --git a/doc/User.md b/doc/User.md new file mode 100644 index 0000000..570877c --- /dev/null +++ b/doc/User.md @@ -0,0 +1,26 @@ +# Skynet Discord Bot +The Skynet bot is designed to make it easy to verify that you are a member of a Club/Society. +The bot will be able to give you member roles for any partnered servers. +It also provides secondary manifests such as granting access to minecraft servers managed by teh Computer Society. + +## Setup +This is to link your Discord account with your UL Wolves account. +**You will only need to do this once**. + +### Setup +1. In a Discord server with the Skynet Bot enter ``/wolves link YOUR_WOLVES_CONTACT_EMAIL`` + link process start + * Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email in the Contact Email here: + * This is most likely your student mail +2. An email will be sent to you with a verification code. + signup email +3. Verify the code using ``/wolves verify CODE_FROM_EMAIL`` in Discord. + verify in discord +4. Once complete your Wolves and Discord accounts will be linked. + +You will get member roles on any Discord that is using the bot that you are a member of. + +### Minecraft +You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society. + +``/wolves link_minecraft MINECRAFT_USERNAME`` diff --git a/flake.lock b/flake.lock index 9f7289a..2c4a9bc 100644 --- a/flake.lock +++ b/flake.lock @@ -32,6 +32,22 @@ "type": "indirect" } }, + "nixpkgs-mozilla": { + "flake": false, + "locked": { + "lastModified": 1744624473, + "narHash": "sha256-S6zT/w5SyAkJ//dYdjbrXgm+6Vkd/k7qqUl4WgZ6jjk=", + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "rev": "2292d4b35aa854e312ad2e95c4bb5c293656f21a", + "type": "github" + }, + "original": { + "owner": "mozilla", + "repo": "nixpkgs-mozilla", + "type": "github" + } + }, "nixpkgs_2": { "locked": { "lastModified": 1722995383, @@ -51,6 +67,7 @@ "inputs": { "naersk": "naersk", "nixpkgs": "nixpkgs_2", + "nixpkgs-mozilla": "nixpkgs-mozilla", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index b48d90b..3a6032f 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,10 @@ inputs = { nixpkgs.url = "nixpkgs/nixos-unstable"; naersk.url = "github:nix-community/naersk"; + nixpkgs-mozilla = { + url = "github:mozilla/nixpkgs-mozilla"; + flake = false; + }; utils.url = "github:numtide/flake-utils"; }; @@ -17,15 +21,33 @@ nixpkgs, utils, naersk, + nixpkgs-mozilla, }: utils.lib.eachDefaultSystem ( system: let - pkgs = (import nixpkgs) {inherit system;}; - naersk' = pkgs.callPackage naersk {}; + overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); + pkgs = (import nixpkgs) { + inherit system; + + overlays = [ + (import nixpkgs-mozilla) + ]; + }; + toolchain = (pkgs.rustChannelOf { + rustToolchain = ./rust-toolchain.toml; + sha256 = "sha256-KUm16pHj+cRedf8vxs/Hd2YWxpOrWZ7UOrwhILdSJBU="; + }).rust; + + naersk' = pkgs.callPackage naersk { + cargo = toolchain; + rustc = toolchain; + }; package_name = "skynet_discord_bot"; desc = "Skynet Discord Bot"; buildInputs = with pkgs; [ openssl + glib + gdk-pixbuf pkg-config rustfmt ]; @@ -36,6 +58,10 @@ pname = "${package_name}"; src = ./.; buildInputs = buildInputs; + postInstall = '' + mkdir $out/config + cp .server-icons.toml $out/config + ''; }; # Run `nix build .#fmt` to run tests fmt = naersk'.buildPackage { @@ -62,7 +88,15 @@ # `nix develop` devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; [rustc cargo pkg-config openssl rustfmt]; + nativeBuildInputs = with pkgs; [rustup rustPlatform.bindgenHook]; + # libraries here + buildInputs = buildInputs; + RUSTC_VERSION = overrides.toolchain.channel; + # https://github.com/rust-lang/rust-bindgen#environment-variables + shellHook = '' + export PATH="''${CARGO_HOME:-~/.cargo}/bin":"$PATH" + export PATH="''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-${pkgs.stdenv.hostPlatform.rust.rustcTarget}/bin":"$PATH" + ''; }; nixosModule = { @@ -88,12 +122,14 @@ wantedBy = []; after = ["network-online.target"]; environment = environment_config; - + path = with pkgs; [ git git-lfs ]; serviceConfig = { Type = "oneshot"; User = "${cfg.user}"; Group = "${cfg.user}"; ExecStart = "${self.defaultPackage."${system}"}/bin/${script}"; + # kill each service if its ran for 9 min + TimeoutStartSec=540; EnvironmentFile = [ "${cfg.env.discord}" "${cfg.env.mail}" @@ -118,12 +154,17 @@ # modify these scripts = { - # every 20 min - "update_data" = "*:0,20,40"; + # every 10 min + "update_data" = "*:0,10,20,30,40,50"; # groups are updated every hour, offset from teh ldap - "update_users" = "*:05:00"; + "update_users" = "*:5,15,25,35,45,55"; + # Committee server has its own timer + "update_committee" = "*:5,15,25,35,45,55"; # minecraft stuff is updated at 5am + # this service does not depend on teh discord cache "update_minecraft" = "5:10:00"; + # server icon gets updated daily at midnight + "update_server-icon" = "0:01:00"; }; in { options.services."${package_name}" = { @@ -183,6 +224,7 @@ after = ["network-online.target"]; wants = []; environment = environment_config; + path = with pkgs; [ git git-lfs ]; serviceConfig = { User = "${cfg.user}"; diff --git a/media/setup_user_01.png b/media/setup_user_01.png new file mode 100644 index 0000000..c8ac1a8 --- /dev/null +++ b/media/setup_user_01.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:684bcaa532d75d90a63512c648c44d9cd12a6e34fce6c2a55bf1d5d4f7446371 +size 42969 diff --git a/media/setup_user_02.png b/media/setup_user_02.png new file mode 100644 index 0000000..34898b9 --- /dev/null +++ b/media/setup_user_02.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e64d8e2322990dcc6446a34e988c6754edc536c3ee1aa8c330e7dadb899152bc +size 72884 diff --git a/media/setup_user_03.png b/media/setup_user_03.png new file mode 100644 index 0000000..f9f4e3a --- /dev/null +++ b/media/setup_user_03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3952b8c2a55604b88a0034f68dd29abd3a72b3e1ced8074bacb358b96f14c7b1 +size 48708 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 8cca5be..0837c1f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.80" +channel = "1.87" diff --git a/src/bin/cleanup_committee.rs b/src/bin/cleanup_committee.rs new file mode 100644 index 0000000..188cc5b --- /dev/null +++ b/src/bin/cleanup_committee.rs @@ -0,0 +1,139 @@ +use serenity::{ + all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent}, + async_trait, + client::{Context, EventHandler}, + model::gateway::GatewayIntents, + Client, +}; +use skynet_discord_bot::{ + common::{ + database::{db_init, DataBase}, + set_roles::committee::db_roles_get, + }, + get_config, Config, +}; +use sqlx::{Pool, Sqlite}; +use std::{process, sync::Arc}; +use tokio::sync::RwLock; + +/// Cleanup teh Committee server +/// +/// This removes any invalid roles/channels which ay have been set up accidentally +/// DO NOT run this locally unless you have a fresh copy of teh live database handy. +#[tokio::main] +async fn main() { + let config = get_config(); + let db = match db_init(&config).await { + Ok(x) => x, + Err(_) => return, + }; + + // Intents are a bitflag, bitwise operations can be used to dictate which intents to use + let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; + // Build our client. + let mut client = Client::builder(&config.discord_token, intents) + .event_handler(Handler {}) + .cache_settings(serenity::cache::Settings::default()) + .await + .expect("Error creating client"); + + { + let mut data = client.data.write().await; + + data.insert::(Arc::new(RwLock::new(config))); + data.insert::(Arc::new(RwLock::new(db))); + } + + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} + +struct Handler; +#[async_trait] +impl EventHandler for Handler { + async fn cache_ready(&self, ctx: Context, _guilds: Vec) { + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config_global = config_lock.read().await; + + let server = config_global.committee_server; + + ctx.shard.chunk_guild(server, Some(2000), false, ChunkGuildFilter::None, None); + + println!("Cache loaded"); + } + + async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent) { + if (chunk.chunk_index + 1) == chunk.chunk_count { + println!("Cache built successfully!"); + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + cleanup(&db, &ctx, &config).await; + // finish up + process::exit(0); + } + } +} + +async fn cleanup(db: &Pool, ctx: &Context, config: &Config) { + let server = config.committee_server; + let committees = db_roles_get(db).await; + + if let Ok(channels) = server.channels(ctx).await { + for (id, channel) in &channels { + let name = &channel.name; + let committee_tmp = committees.iter().filter(|x| &x.name_channel == name).collect::>(); + let committee = match committee_tmp.first() { + // if there are no committees which match then this is not a channelw e care about + None => { + continue; + } + Some(x) => x, + }; + + // if the id of the channel does not match then remove it + if id != &committee.id_channel { + println!("Deleting Channel - ID: {} Name: {}", id, &channel.name); + if let Err(e) = channel.delete(ctx).await { + dbg!(e); + } + } + } + } + + if let Ok(mut roles) = server.roles(ctx).await { + for (id, role) in &mut roles { + let name = &role.name; + let committee_tmp = committees.iter().filter(|x| &x.name_role == name).collect::>(); + let committee = match committee_tmp.first() { + // if there are no committees which match then this is not a channelw e care about + None => { + continue; + } + Some(x) => x, + }; + + // if the id of the role does not match then remove it + if id != &committee.id_role { + println!("Deleting Role - ID: {} Name: {}", id, &role.name); + if let Err(e) = role.delete(ctx).await { + dbg!(e); + } + } + } + } +} diff --git a/src/bin/update_committee.rs b/src/bin/update_committee.rs new file mode 100644 index 0000000..b8cc6c2 --- /dev/null +++ b/src/bin/update_committee.rs @@ -0,0 +1,74 @@ +use serenity::{ + all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent}, + async_trait, + client::{Context, EventHandler}, + model::gateway::GatewayIntents, + Client, +}; +use skynet_discord_bot::{ + common::{ + database::{db_init, DataBase}, + set_roles::committee, + }, + get_config, Config, +}; +use std::{process, sync::Arc}; +use tokio::sync::RwLock; + +#[tokio::main] +async fn main() { + let config = get_config(); + let db = match db_init(&config).await { + Ok(x) => x, + Err(_) => return, + }; + + // Intents are a bitflag, bitwise operations can be used to dictate which intents to use + let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; + // Build our client. + let mut client = Client::builder(&config.discord_token, intents) + .event_handler(Handler {}) + .cache_settings(serenity::cache::Settings::default()) + .await + .expect("Error creating client"); + + { + let mut data = client.data.write().await; + + data.insert::(Arc::new(RwLock::new(config))); + data.insert::(Arc::new(RwLock::new(db))); + } + + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} + +struct Handler; +#[async_trait] +impl EventHandler for Handler { + async fn cache_ready(&self, ctx: Context, _guilds: Vec) { + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config_global = config_lock.read().await; + + let server = config_global.committee_server; + + ctx.shard.chunk_guild(server, Some(2000), false, ChunkGuildFilter::None, None); + + println!("Cache loaded"); + } + + async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent) { + if (chunk.chunk_index + 1) == chunk.chunk_count { + println!("Cache built successfully!"); + // u[date committee server + committee::check_committee(&ctx).await; + + // finish up + process::exit(0); + } + } +} diff --git a/src/bin/update_data.rs b/src/bin/update_data.rs index 22bd5bd..8f73ce9 100644 --- a/src/bin/update_data.rs +++ b/src/bin/update_data.rs @@ -4,7 +4,13 @@ use serenity::{ model::gateway::{GatewayIntents, Ready}, Client, }; -use skynet_discord_bot::{db_init, get_config, get_data::get_wolves, Config, DataBase}; +use skynet_discord_bot::{ + common::{ + database::{db_init, DataBase}, + wolves::{cns::get_wolves, committees::get_cns}, + }, + get_config, Config, +}; use std::{process, sync::Arc}; use tokio::sync::RwLock; @@ -13,7 +19,10 @@ async fn main() { let config = get_config(); let db = match db_init(&config).await { Ok(x) => x, - Err(_) => return, + Err(e) => { + dbg!(e); + return; + } }; // Intents are a bitflag, bitwise operations can be used to dictate which intents to use @@ -21,6 +30,7 @@ async fn main() { // Build our client. let mut client = Client::builder(&config.discord_token, intents) .event_handler(Handler {}) + .cache_settings(serenity::cache::Settings::default()) .await .expect("Error creating client"); @@ -32,7 +42,7 @@ async fn main() { } if let Err(why) = client.start().await { - println!("Client error: {:?}", why); + println!("Client error: {why:?}"); } } @@ -43,8 +53,12 @@ impl EventHandler for Handler { let ctx = Arc::new(ctx); println!("{} is connected!", ready.user.name); + // get the data for each individual club/soc get_wolves(&ctx).await; + // get teh data for the clubs/socs committees + get_cns(&ctx).await; + // finish up process::exit(0); } diff --git a/src/bin/update_minecraft.rs b/src/bin/update_minecraft.rs index 72aad1c..f7c24e0 100644 --- a/src/bin/update_minecraft.rs +++ b/src/bin/update_minecraft.rs @@ -1,4 +1,10 @@ -use skynet_discord_bot::{db_init, get_config, get_minecraft_config, update_server, whitelist_wipe}; +use skynet_discord_bot::{ + common::{ + database::db_init, + minecraft::{get_minecraft_config, update_server, whitelist_wipe}, + }, + get_config, +}; use std::collections::HashSet; #[tokio::main] diff --git a/src/bin/update_server-icon.rs b/src/bin/update_server-icon.rs new file mode 100644 index 0000000..56957da --- /dev/null +++ b/src/bin/update_server-icon.rs @@ -0,0 +1,72 @@ +use serenity::{ + async_trait, + client::{Context, EventHandler}, + model::gateway::{GatewayIntents, Ready}, + Client, +}; +use skynet_discord_bot::{ + common::{ + database::{db_init, DataBase}, + server_icon::{get_config_icons, update_icon}, + }, + get_config, Config, +}; +use std::{process, sync::Arc}; +use tokio::sync::RwLock; + +#[tokio::main] +async fn main() { + let config = get_config(); + let db = match db_init(&config).await { + Ok(x) => x, + Err(_) => return, + }; + + // Intents are a bitflag, bitwise operations can be used to dictate which intents to use + let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; + // Build our client. + let mut client = Client::builder(&config.discord_token, intents) + .event_handler(Handler {}) + .cache_settings(serenity::cache::Settings::default()) + .await + .expect("Error creating client"); + + { + let mut data = client.data.write().await; + + data.insert::(Arc::new(RwLock::new(config))); + data.insert::(Arc::new(RwLock::new(db))); + } + + if let Err(why) = client.start().await { + println!("Client error: {why:?}"); + } +} + +struct Handler; +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, ready: Ready) { + let ctx = Arc::new(ctx); + println!("{} is connected!", ready.user.name); + + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + let config_global = config_lock.read().await; + let config_toml = get_config_icons::minimal(); + + update_icon::update_icon_main(&ctx, &db, &config_global, &config_toml).await; + + // finish up + process::exit(0); + } +} diff --git a/src/bin/update_users.rs b/src/bin/update_users.rs index d46c0e5..0cb11ac 100644 --- a/src/bin/update_users.rs +++ b/src/bin/update_users.rs @@ -1,10 +1,18 @@ use serenity::{ + all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent}, async_trait, client::{Context, EventHandler}, - model::gateway::{GatewayIntents, Ready}, + model::gateway::GatewayIntents, Client, }; -use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles, Config, DataBase}; +use skynet_discord_bot::{ + common::{ + database::{db_init, get_server_config_bulk, DataBase}, + set_roles::normal, + }, + get_config, Config, +}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{process, sync::Arc}; use tokio::sync::RwLock; @@ -20,7 +28,11 @@ async fn main() { let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; // Build our client. let mut client = Client::builder(&config.discord_token, intents) - .event_handler(Handler {}) + .event_handler(Handler { + server_count: Default::default(), + server_cached: Default::default(), + }) + .cache_settings(serenity::cache::Settings::default()) .await .expect("Error creating client"); @@ -32,25 +44,41 @@ async fn main() { } if let Err(why) = client.start().await { - println!("Client error: {:?}", why); + println!("Client error: {why:?}"); } } -struct Handler; +struct Handler { + server_count: AtomicUsize, + server_cached: AtomicUsize, +} #[async_trait] impl EventHandler for Handler { - async fn ready(&self, ctx: Context, ready: Ready) { - let ctx = Arc::new(ctx); - println!("{} is connected!", ready.user.name); + async fn cache_ready(&self, ctx: Context, guilds: Vec) { + self.server_count.swap(guilds.len(), Ordering::SeqCst); + for guild in guilds { + ctx.shard.chunk_guild(guild, Some(2000), false, ChunkGuildFilter::None, None); + } + println!("Cache loaded {}", &self.server_count.load(Ordering::SeqCst)); + } - bulk_check(Arc::clone(&ctx)).await; + async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent) { + if (chunk.chunk_index + 1) == chunk.chunk_count { + self.server_cached.fetch_add(1, Ordering::SeqCst); + if (self.server_cached.load(Ordering::SeqCst) + 1) == self.server_count.load(Ordering::SeqCst) { + println!("Cache built successfully!"); - // finish up - process::exit(0); + // this goes into each server and sets roles for each wolves member + check_bulk(&ctx).await; + + // finish up + process::exit(0); + } + } } } -async fn bulk_check(ctx: Arc) { +async fn check_bulk(ctx: &Context) { let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Config in TypeMap.").clone() @@ -59,6 +87,6 @@ async fn bulk_check(ctx: Arc) { let db = db_lock.read().await; for server_config in get_server_config_bulk(&db).await { - set_roles::update_server(&ctx, &server_config, &[], &[]).await; + normal::update_server(ctx, &server_config, &[], &[]).await; } } diff --git a/src/commands/add_server.rs b/src/commands/add_server.rs index e140283..0c7dd2f 100644 --- a/src/commands/add_server.rs +++ b/src/commands/add_server.rs @@ -1,98 +1,61 @@ use serenity::{ - builder::CreateApplicationCommand, + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, client::Context, - model::{ - application::interaction::application_command::ApplicationCommandInteraction, - prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, - }, }; -use skynet_discord_bot::get_data::get_wolves; -use skynet_discord_bot::{get_server_config, is_admin, set_roles::update_server, DataBase, Servers}; +use skynet_discord_bot::common::{ + database::{get_server_config, DataBase, Servers}, + set_roles::normal::update_server, + wolves::cns::get_wolves, +}; use sqlx::{Error, Pool, Sqlite}; -pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - // check if user has high enough permisssions - if let Some(msg) = is_admin(command, ctx).await { - return msg; - } - - let api_key = if let CommandDataOptionValue::String(key) = command - .data - .options - .first() - .expect("Expected user option") - .resolved - .as_ref() - .expect("Expected user object") +pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(options), + .. + }) = command.data.options.first() { - key + options + } else { + return "Please provide sub options".to_string(); + }; + + let wolves_api = if let Some(x) = sub_options.first() { + match &x.value { + CommandDataOptionValue::String(key) => key.to_string(), + _ => return "Please provide a wolves API key".to_string(), + } } else { return "Please provide a wolves API key".to_string(); }; - let role_current = if let CommandDataOptionValue::Role(role) = command - .data - .options - .get(1) - .expect("Expected role option") - .resolved - .as_ref() - .expect("Expected role object") - { - role.id.to_owned() + let role_current = if let Some(x) = sub_options.get(1) { + match &x.value { + CommandDataOptionValue::Role(role) => role.to_owned(), + _ => return "Please provide a valid role for ``Role Current``".to_string(), + } } else { return "Please provide a valid role for ``Role Current``".to_string(); }; - let mut role_past = None; - if let Some(x) = command.data.options.get(5) { - if let Some(CommandDataOptionValue::Role(role)) = &x.resolved { - role_past = Some(role.id.to_owned()); + let role_past = if let Some(x) = sub_options.get(5) { + match &x.value { + CommandDataOptionValue::Role(role) => Some(role.to_owned()), + _ => None, } + } else { + None }; - let bot_channel_id = if let CommandDataOptionValue::Channel(channel) = command - .data - .options - .get(2) - .expect("Expected channel option") - .resolved - .as_ref() - .expect("Expected channel object") - { - channel.id.to_owned() + let bot_channel_id = if let Some(x) = sub_options.get(2) { + match &x.value { + CommandDataOptionValue::Channel(channel) => channel.to_owned(), + _ => return "Please provide a valid channel for ``Bot Channel``".to_string(), + } } else { return "Please provide a valid channel for ``Bot Channel``".to_string(); }; - let server_name = if let CommandDataOptionValue::String(name) = command - .data - .options - .get(3) - .expect("Expected Server Name option") - .resolved - .as_ref() - .expect("Expected Server Name object") - { - name - } else { - &"UL Computer Society".to_string() - }; - - let wolves_link = if let CommandDataOptionValue::String(wolves) = command - .data - .options - .get(4) - .expect("Expected Wolves Link option") - .resolved - .as_ref() - .expect("Expected Server Name object") - { - wolves - } else { - &"https://ulwolves.ie/society/computer".to_string() - }; - let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() @@ -101,92 +64,41 @@ pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> Stri let server_data = Servers { server: command.guild_id.unwrap_or_default(), - wolves_api: api_key.to_owned(), + wolves_api, + wolves_id: 0, role_past, role_current, member_past: 0, member_current: 0, bot_channel_id, - server_name: server_name.to_owned(), - wolves_link: wolves_link.to_string(), }; match add_server(&db, ctx, &server_data).await { Ok(_) => {} Err(e) => { - println!("{:?}", e); - return format!("Failure to insert into Servers {:?}", server_data); + println!("{e:?}"); + return format!("Failure to insert into Servers {server_data:?}"); } } "Added/Updated server info".to_string() } -pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("add") - .description("Enable the bot for this discord") - .create_option(|option| { - option - .name("api_key") - .description("UL Wolves API Key") - .kind(CommandOptionType::String) - .required(true) - }) - .create_option(|option| { - option - .name("role_current") - .description("Role for Current members") - .kind(CommandOptionType::Role) - .required(true) - }) - .create_option(|option| { - option - .name("bot_channel") - .description("Safe space for folks to use the bot commands.") - .kind(CommandOptionType::Channel) - .required(true) - }) - .create_option(|option| { - option - .name("server_name") - .description("Name of the Discord Server.") - .kind(CommandOptionType::String) - .required(true) - }) - .create_option(|option| { - option - .name("wolves_link") - .description("Link to the Club/Society on UL Wolves.") - .kind(CommandOptionType::String) - .required(true) - }) - .create_option(|option| { - option - .name("role_past") - .description("Role for Past members") - .kind(CommandOptionType::Role) - .required(false) - }) -} - async fn add_server(db: &Pool, ctx: &Context, server: &Servers) -> Result, Error> { let existing = get_server_config(db, &server.server).await; - let role_past = server.role_past.map(|x| *x.as_u64() as i64); + let role_past = server.role_past.map(|x| x.get() as i64); let insert = sqlx::query_as::<_, Servers>( " - INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current, bot_channel_id, server_name, wolves_link) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current, bot_channel_id) + VALUES (?1, ?2, ?3, ?4, ?5) ", ) - .bind(*server.server.as_u64() as i64) + .bind(server.server.get() as i64) .bind(&server.wolves_api) .bind(role_past) - .bind(*server.role_current.as_u64() as i64) - .bind(*server.bot_channel_id.as_u64() as i64) - .bind(&server.server_name) - .bind(&server.wolves_link) + .bind(server.role_current.get() as i64) + .bind(server.bot_channel_id.get() as i64) .fetch_optional(db) .await; diff --git a/src/commands/committee.rs b/src/commands/committee.rs index 487171b..944fbc0 100644 --- a/src/commands/committee.rs +++ b/src/commands/committee.rs @@ -1,311 +1,27 @@ -use lettre::{ - message::{header, MultiPart, SinglePart}, - transport::smtp::{self, authentication::Credentials}, - Message, SmtpTransport, Transport, -}; -use maud::html; -use serenity::{ - builder::CreateApplicationCommand, - client::Context, - model::{ - application::interaction::application_command::ApplicationCommandInteraction, - id::UserId, - prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, - }, -}; -use skynet_discord_bot::{random_string, Config, DataBase}; -use sqlx::{Pool, Sqlite}; +use serenity::all::{CommandOptionType, CreateCommand, CreateCommandOption}; -pub mod link { - use super::*; - use serenity::model::id::GuildId; - use skynet_discord_bot::Committee; - - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - let committee_server = GuildId(1220150752656363520); - match command.guild_id { - None => { - return "Not in correct discord server.".to_string(); - } - Some(x) => { - if x != committee_server { - return "Not in correct discord server.".to_string(); - } - } - } - - let option = command - .data - .options - .first() - .expect("Expected email option") - .resolved - .as_ref() - .expect("Expected email object"); - - let email = if let CommandDataOptionValue::String(email) = option { - email.trim() - } else { - return "Please provide a valid committee email.".to_string(); - }; - - // fail early - if !email.ends_with("@ulwolves.ie") { - return "Please use a @ulwolves.ie address you have access to.".to_string(); - } - - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Databse in TypeMap.").clone() - }; - let db = db_lock.read().await; - - let config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Config in TypeMap.").clone() - }; - let config = config_lock.read().await; - - if get_server_member_discord(&db, &command.user.id).await.is_some() { - return "Already linked".to_string(); - } - - if get_verify_from_db(&db, &command.user.id).await.is_some() { - return "Linking already in process, please check email.".to_string(); - } - - // generate a auth key - let auth = random_string(20); - match send_mail(&config, email, &auth, &command.user.name) { - Ok(_) => match save_to_db(&db, email, &auth, &command.user.id).await { - Ok(_) => {} - Err(e) => { - return format!("Unable to save to db {} {e:?}", email); - } - }, - Err(e) => { - return format!("Unable to send mail to {} {e:?}", email); - } - } - - format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email) - } - - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("link_committee") - .description("Verify you are a committee member") - .create_option(|option| { - option - .name("email") - .description("UL Wolves Committee Email") - .kind(CommandOptionType::String) - .required(true) - }) - } - - pub async fn get_server_member_discord(db: &Pool, user: &UserId) -> Option { - sqlx::query_as::<_, Committee>( - r#" - SELECT * - FROM committee - WHERE discord = ? - "#, +pub fn register() -> CreateCommand { + CreateCommand::new("committee") + .description("Commands related to what committees can do") + .default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "add", "Enable the bot for this discord") + .add_sub_option(CreateCommandOption::new(CommandOptionType::String, "api_key", "UL Wolves API Key").required(true)) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_current", "Role for Current members").required(true)) + .add_sub_option( + CreateCommandOption::new(CommandOptionType::Channel, "bot_channel", "Safe space for folks to use the bot commands.").required(true), + ) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_past", "Role for Past members").required(false)), ) - .bind(*user.as_u64() as i64) - .fetch_one(db) - .await - .ok() - } - - fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result { - let sender = format!("UL Computer Society <{}>", &config.mail_user); - - // Create the html we want to send. - let html = html! { - head { - title { "Hello from Skynet!" } - style type="text/css" { - "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" - } - } - div style="display: flex; flex-direction: column; align-items: center;" { - h2 { "Hello from Skynet!" } - // Substitute in the name of our recipient. - p { "Hi " (user) "," } - p { - "Please use " pre { "/verify_committee code: " (auth)} " to verify your discord account." - } - p { - "Skynet Team" - br; - "UL Computer Society" - } - } - }; - - let body_text = format!( - r#" - Hi {user} - - Please use "/verify_committee code: {auth}" to verify your discord account. - - Skynet Team - UL Computer Society - "# - ); - - // Build the message. - let email = Message::builder() - .from(sender.parse().unwrap()) - .to(mail.parse().unwrap()) - .subject("Skynet-Discord: Link Committee.") - .multipart( - // This is composed of two parts. - // also helps not trip spam settings (uneven number of url's - MultiPart::alternative() - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), - ) - .expect("failed to build email"); - - let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); - - // Open a remote connection to gmail using STARTTLS - let mailer = SmtpTransport::starttls_relay(&config.mail_smtp)?.credentials(creds).build(); - - // Send the email - mailer.send(&email) - } - - pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { - sqlx::query_as::<_, Committee>( - r#" - SELECT * - FROM committee - WHERE discord = ? - "#, + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "roles_adder", "Combine roles together to an new one") + .add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_a", "A role you want to add to Role B").required(true)) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_b", "A role you want to add to Role A").required(true)) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_c", "Sum of A and B").required(true)) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "delete", "Delete this entry.").required(false)), ) - .bind(*user.as_u64() as i64) - .fetch_one(db) - .await - .ok() - } - - async fn save_to_db(db: &Pool, email: &str, auth: &str, user: &UserId) -> Result, sqlx::Error> { - sqlx::query_as::<_, Committee>( - " - INSERT INTO committee (email, discord, auth_code) - VALUES (?1, ?2, ?3) - ", + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommandGroup, "icon", "Committee commands for the server icon") + .add_sub_option(CreateCommandOption::new(CommandOptionType::SubCommand, "change", "Change the server icon.")), ) - .bind(email.to_owned()) - .bind(*user.as_u64() as i64) - .bind(auth.to_owned()) - .fetch_optional(db) - .await - } -} - -pub mod verify { - use super::*; - use crate::commands::committee::link::get_verify_from_db; - use serenity::model::id::{GuildId, RoleId}; - use serenity::model::user::User; - use skynet_discord_bot::Committee; - use sqlx::Error; - - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - let committee_server = GuildId(1220150752656363520); - match command.guild_id { - None => { - return "Not in correct discord server.".to_string(); - } - Some(x) => { - if x != committee_server { - return "Not in correct discord server.".to_string(); - } - } - } - - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Database in TypeMap.").clone() - }; - let db = db_lock.read().await; - - // check if user has used /link_committee - let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { - x - } else { - return "Please use /link_committee first".to_string(); - }; - - let option = command - .data - .options - .first() - .expect("Expected code option") - .resolved - .as_ref() - .expect("Expected code object"); - - let code = if let CommandDataOptionValue::String(code) = option { - code - } else { - return "Please provide a verification code".to_string(); - }; - - if &details.auth_code != code { - return "Invalid verification code".to_string(); - } - - match set_discord(&db, &command.user.id).await { - Ok(_) => { - // get teh right roles for the user - set_server_roles(&command.user, ctx).await; - "Discord username linked to Wolves for committee".to_string() - } - Err(e) => { - println!("{:?}", e); - "Failed to save, please try /link_committee again".to_string() - } - } - } - - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("verify_committee") - .description("Verify Wolves Committee Email") - .create_option(|option| { - option - .name("code") - .description("Code from verification email") - .kind(CommandOptionType::String) - .required(true) - }) - } - - async fn set_discord(db: &Pool, discord: &UserId) -> Result, Error> { - sqlx::query_as::<_, Committee>( - " - UPDATE committee - SET committee = 1 - WHERE discord = ? - ", - ) - .bind(*discord.as_u64() as i64) - .fetch_optional(db) - .await - } - - async fn set_server_roles(discord: &User, ctx: &Context) { - let committee_server = GuildId(1220150752656363520); - if let Ok(mut member) = committee_server.member(&ctx.http, &discord.id).await { - let committee_member = RoleId(1226602779968274573); - if let Err(e) = member.add_role(&ctx, committee_member).await { - println!("{:?}", e); - } - } - } } diff --git a/src/commands/count.rs b/src/commands/count.rs new file mode 100644 index 0000000..2368bca --- /dev/null +++ b/src/commands/count.rs @@ -0,0 +1,261 @@ +pub mod committee { + + // get the list of all the current clubs/socs members + + use serenity::all::{ + CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, + }; + use skynet_discord_bot::common::{database::DataBase, set_roles::committee::db_roles_get}; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(key), + .. + }) = command.data.options.first() + { + key + } else { + return "Please provide a wolves API key".to_string(); + }; + + let all = if let Some(x) = sub_options.first() { + match x.value { + CommandDataOptionValue::Boolean(y) => y, + _ => false, + } + } else { + false + }; + + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let mut cs = vec![]; + // pull it from a DB + for committee in db_roles_get(&db).await { + if !all && committee.count == 0 { + continue; + } + cs.push((committee.count, committee.name_role.to_owned())); + } + + cs.sort_by_key(|(count, _)| *count); + cs.reverse(); + + // msg can be a max 2000 chars long + let mut limit = 2000 - 3; + + let mut response = vec!["```".to_string()]; + for (count, name) in cs { + let leading = if count < 10 { " " } else { "" }; + + let line = format!("{leading}{count} {name}"); + + let length = line.len() + 1; + + if length < (limit + 3) { + response.push(line); + limit -= length; + } else { + break; + } + } + response.push("```".to_string()); + + response.join("\n") + } + + pub fn register() -> CreateCommand { + CreateCommand::new("count") + .description("Count Committee Members") + // All Committee members are able to add reactions to posts + .default_member_permissions(serenity::model::Permissions::ADD_REACTIONS) + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "committee", "List out the Committee Roles Numbers") + .add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "all", "List out all the Committee Roles Numbers").required(false)), + ) + } +} + +pub mod servers { + // get the list of all the current clubs/socs + use serde::{Deserialize, Serialize}; + use serenity::all::{CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption}; + use skynet_discord_bot::{ + common::{ + database::{get_server_config_bulk, DataBase}, + set_roles::committee::get_committees, + }, + get_now_iso, + }; + use sqlx::{Pool, Sqlite}; + use std::collections::HashMap; + + pub async fn run(_command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let mut committees = HashMap::new(); + if let Some(x) = get_committees(&db).await { + for committee in x { + committees.insert(committee.id, committee.to_owned()); + } + } + + let mut cs = vec![]; + // pull it from a DB + for server_config in get_server_config_bulk(&db).await { + if let Some(x) = committees.get(&server_config.wolves_id) { + cs.push((server_config.member_current, server_config.member_past, x.name_full.to_owned())); + } + } + + // get all members + let (wolves_current, wolves_past, total) = get_wolves_total(&db).await; + cs.push((total, total, String::from("Skynet Network"))); + cs.push((wolves_current, wolves_past, String::from("Clubs/Socs Servers"))); + + // treat teh committee server as its own thing + let committee_current = get_wolves_committee(&db).await; + cs.push((committee_current, committee_current, String::from("Committee Server"))); + + cs.sort_by_key(|(current, _, _)| *current); + cs.reverse(); + + // msg can be a max 2000 chars long + let mut limit = 2000 - 3; + + let mut response = vec!["```".to_string()]; + for (current, past, name) in cs { + let current_leading = if current < 10 { + " " + } else if current < 100 { + " " + } else { + "" + }; + let past_leading = if past < 10 { + " " + } else if past < 100 { + " " + } else { + "" + }; + + let line = format!("{current_leading}{current} {past_leading}{past} {name}"); + + let length = line.len() + 1; + + // +3 is to account for the closing fense + if length < (limit + 3) { + response.push(line); + limit -= length; + } else { + break; + } + } + response.push("```".to_string()); + + response.join("\n") + } + + #[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)] + pub struct Count { + pub count: i64, + } + + async fn get_wolves_total(db: &Pool) -> (i64, i64, i64) { + let current = match sqlx::query_as::<_, Count>( + r#" + SELECT COUNT(DISTINCT id_wolves) as count + FROM server_members + JOIN wolves USING (id_wolves) + WHERE ( + wolves.discord IS NOT NULL + AND server_members.expiry > ? + ) + "#, + ) + .bind(get_now_iso(true)) + .fetch_one(db) + .await + { + Ok(res) => res.count, + Err(e) => { + dbg!(e); + 0 + } + }; + + let cns = match sqlx::query_as::<_, Count>( + r#" + SELECT COUNT(DISTINCT id_wolves) as count + FROM server_members + JOIN wolves USING (id_wolves) + WHERE wolves.discord IS NOT NULL + "#, + ) + .bind(get_now_iso(true)) + .fetch_one(db) + .await + { + Ok(res) => res.count, + Err(e) => { + dbg!(e); + 0 + } + }; + + let total = match sqlx::query_as::<_, Count>( + r#" + SELECT COUNT(DISTINCT id_wolves) as count + FROM wolves + WHERE discord IS NOT NULL + "#, + ) + .fetch_one(db) + .await + { + Ok(res) => res.count, + Err(e) => { + dbg!(e); + 0 + } + }; + + (current, cns, total) + } + + async fn get_wolves_committee(db: &Pool) -> i64 { + // expiry + match sqlx::query_as::<_, Count>( + " + SELECT count + FROM committee_roles + WHERE id_wolves = '0' + ", + ) + .fetch_one(db) + .await + { + Ok(res) => res.count, + Err(e) => { + dbg!(e); + 0 + } + } + } + + pub fn register() -> CreateCommand { + CreateCommand::new("count") + .description("Count the servers") + .default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) + .add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "servers", "List out all servers using the skynet bot")) + } +} diff --git a/src/commands/link_email.rs b/src/commands/link_email.rs deleted file mode 100644 index 4961dbb..0000000 --- a/src/commands/link_email.rs +++ /dev/null @@ -1,381 +0,0 @@ -use lettre::{ - message::{header, MultiPart, SinglePart}, - transport::smtp::{self, authentication::Credentials}, - Message, SmtpTransport, Transport, -}; -use maud::html; -use serenity::{ - builder::CreateApplicationCommand, - client::Context, - model::{ - application::interaction::application_command::ApplicationCommandInteraction, - id::UserId, - prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, - }, -}; -use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify}; -use sqlx::{Pool, Sqlite}; -pub mod link { - use super::*; - - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Databse in TypeMap.").clone() - }; - let db = db_lock.read().await; - - let config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Config in TypeMap.").clone() - }; - let config = config_lock.read().await; - - if get_server_member_discord(&db, &command.user.id).await.is_some() { - return "Already linked".to_string(); - } - - db_pending_clear_expired(&db).await; - - if get_verify_from_db(&db, &command.user.id).await.is_some() { - return "Linking already in process, please check email.".to_string(); - } - - let option = command - .data - .options - .first() - .expect("Expected email option") - .resolved - .as_ref() - .expect("Expected email object"); - - let email = if let CommandDataOptionValue::String(email) = option { - email.trim() - } else { - return "Please provide a valid user".to_string(); - }; - - // check if email exists - let details = match get_server_member_email(&db, email).await { - None => { - return "Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string() - } - Some(x) => x, - }; - - if details.discord.is_some() { - return "Email already verified".to_string(); - } - - // generate a auth key - let auth = random_string(20); - match send_mail(&config, &details, &auth, &command.user.name) { - Ok(_) => match save_to_db(&db, &details, &auth, &command.user.id).await { - Ok(_) => {} - Err(e) => { - return format!("Unable to save to db {} {e:?}", &details.email); - } - }, - Err(e) => { - return format!("Unable to send mail to {} {e:?}", &details.email); - } - } - - format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email) - } - - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command - .name("link_wolves") - .description("Set Wolves Email") - .create_option(|option| option.name("email").description("UL Wolves Email").kind(CommandOptionType::String).required(true)) - } - - pub async fn get_server_member_discord(db: &Pool, user: &UserId) -> Option { - sqlx::query_as::<_, Wolves>( - r#" - SELECT * - FROM wolves - WHERE discord = ? - "#, - ) - .bind(*user.as_u64() as i64) - .fetch_one(db) - .await - .ok() - } - - async fn get_server_member_email(db: &Pool, email: &str) -> Option { - sqlx::query_as::<_, Wolves>( - r#" - SELECT * - FROM wolves - WHERE email = ? - "#, - ) - .bind(email) - .fetch_one(db) - .await - .ok() - } - - fn send_mail(config: &Config, email: &Wolves, auth: &str, user: &str) -> Result { - let mail = &email.email; - let discord = "https://discord.skynet.ie"; - let sender = format!("UL Computer Society <{}>", &config.mail_user); - - // Create the html we want to send. - let html = html! { - head { - title { "Hello from Skynet!" } - style type="text/css" { - "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" - } - } - div style="display: flex; flex-direction: column; align-items: center;" { - h2 { "Hello from Skynet!" } - // Substitute in the name of our recipient. - p { "Hi " (user) "," } - p { - "Please use " pre { "/verify code: " (auth)} " to verify your discord account." - } - p { - "If you have issues please refer to our Discord server:" - br; - a href=(discord) { (discord) } - } - p { - "Skynet Team" - br; - "UL Computer Society" - } - } - }; - - let body_text = format!( - r#" - Hi {user} - - Please use "/verify code: {auth}" to verify your discord account. - - If you have issues please refer to our Discord server: - {discord} - - Skynet Team - UL Computer Society - "# - ); - - // Build the message. - let email = Message::builder() - .from(sender.parse().unwrap()) - .to(mail.parse().unwrap()) - .subject("Skynet-Discord: Link Wolves.") - .multipart( - // This is composed of two parts. - // also helps not trip spam settings (uneven number of url's - MultiPart::alternative() - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) - .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), - ) - .expect("failed to build email"); - - let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); - - // Open a remote connection to gmail using STARTTLS - let mailer = SmtpTransport::starttls_relay(&config.mail_smtp)?.credentials(creds).build(); - - // Send the email - mailer.send(&email) - } - - pub async fn db_pending_clear_expired(pool: &Pool) -> Option { - sqlx::query_as::<_, WolvesVerify>( - r#" - DELETE - FROM wolves_verify - WHERE date_expiry < ? - "#, - ) - .bind(get_now_iso(true)) - .fetch_one(pool) - .await - .ok() - } - - pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { - sqlx::query_as::<_, WolvesVerify>( - r#" - SELECT * - FROM wolves_verify - WHERE discord = ? - "#, - ) - .bind(*user.as_u64() as i64) - .fetch_one(db) - .await - .ok() - } - - async fn save_to_db(db: &Pool, record: &Wolves, auth: &str, user: &UserId) -> Result, sqlx::Error> { - sqlx::query_as::<_, WolvesVerify>( - " - INSERT INTO wolves_verify (email, discord, auth_code, date_expiry) - VALUES (?1, ?2, ?3, ?4) - ", - ) - .bind(record.email.to_owned()) - .bind(*user.as_u64() as i64) - .bind(auth.to_owned()) - .bind(get_now_iso(false)) - .fetch_optional(db) - .await - } -} - -pub mod verify { - use super::*; - use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db}; - use serenity::model::user::User; - use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers}; - use sqlx::Error; - - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Databse in TypeMap.").clone() - }; - let db = db_lock.read().await; - - // check if user has used /link_wolves - let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { - x - } else { - return "Please use /link_wolves first".to_string(); - }; - - let option = command - .data - .options - .first() - .expect("Expected code option") - .resolved - .as_ref() - .expect("Expected code object"); - - let code = if let CommandDataOptionValue::String(code) = option { - code - } else { - return "Please provide a verification code".to_string(); - }; - - db_pending_clear_expired(&db).await; - - if &details.auth_code != code { - return "Invalid verification code".to_string(); - } - - match db_pending_clear_successful(&db, &command.user.id).await { - Ok(_) => { - return match set_discord(&db, &command.user.id, &details.email).await { - Ok(_) => { - // get teh right roles for the user - set_server_roles(&db, &command.user, ctx).await; - "Discord username linked to Wolves".to_string() - } - Err(e) => { - println!("{:?}", e); - "Failed to save, please try /link_wolves again".to_string() - } - }; - } - Err(e) => println!("{:?}", e), - } - - "Failed to verify".to_string() - } - - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("verify").description("Verify Wolves Email").create_option(|option| { - option - .name("code") - .description("Code from verification email") - .kind(CommandOptionType::String) - .required(true) - }) - } - - async fn db_pending_clear_successful(pool: &Pool, user: &UserId) -> Result, Error> { - sqlx::query_as::<_, WolvesVerify>( - r#" - DELETE - FROM wolves_verify - WHERE discord = ? - "#, - ) - .bind(*user.as_u64() as i64) - .fetch_optional(pool) - .await - } - - async fn set_discord(db: &Pool, discord: &UserId, email: &str) -> Result, Error> { - sqlx::query_as::<_, Wolves>( - " - UPDATE wolves - SET discord = ? - WHERE email = ? - ", - ) - .bind(*discord.as_u64() as i64) - .bind(email) - .fetch_optional(db) - .await - } - - async fn set_server_roles(db: &Pool, discord: &User, ctx: &Context) { - if let Ok(servers) = get_servers(db, &discord.id).await { - for server in servers { - if let Ok(mut member) = server.server.member(&ctx.http, &discord.id).await { - if let Some(config) = get_server_config(db, &server.server).await { - let Servers { - role_past, - role_current, - .. - } = config; - - let mut roles = vec![]; - - if let Some(role) = &role_past { - if !member.roles.contains(role) { - roles.push(role.to_owned()); - } - } - - if !member.roles.contains(&role_current) { - roles.push(role_current.to_owned()); - } - - if let Err(e) = member.add_roles(&ctx, &roles).await { - println!("{:?}", e); - } - } - } - } - } - } - - async fn get_servers(db: &Pool, discord: &UserId) -> Result, Error> { - sqlx::query_as::<_, ServerMembersWolves>( - " - SELECT * - FROM server_members - JOIN wolves USING (id_wolves) - WHERE discord = ? - ", - ) - .bind(*discord.as_u64() as i64) - .fetch_all(db) - .await - } -} diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs index b6b6dd5..a4d1b1b 100644 --- a/src/commands/minecraft.rs +++ b/src/commands/minecraft.rs @@ -1,35 +1,28 @@ -use serenity::{ - builder::CreateApplicationCommand, - client::Context, - model::{ - application::interaction::application_command::ApplicationCommandInteraction, - prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, - }, -}; +use serenity::client::Context; -use skynet_discord_bot::DataBase; +use skynet_discord_bot::common::database::DataBase; use sqlx::{Pool, Sqlite}; pub(crate) mod user { use super::*; pub(crate) mod add { use super::*; - use crate::commands::link_email::link::get_server_member_discord; - use serenity::model::id::UserId; - use skynet_discord_bot::{whitelist_update, Config, Minecraft, Wolves}; + use crate::commands::wolves::link::get_server_member_discord; + use serde::{Deserialize, Serialize}; + use serenity::{ + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, + model::id::UserId, + }; + use skynet_discord_bot::{ + common::{ + database::Wolves, + minecraft::{whitelist_update, Minecraft}, + }, + Config, + }; use sqlx::Error; - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("link_minecraft").description("Link your minecraft account").create_option(|option| { - option - .name("minecraft-username") - .description("Your Minecraft username") - .kind(CommandOptionType::String) - .required(true) - }) - } - - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() @@ -44,36 +37,71 @@ pub(crate) mod user { // user has to have previously linked with wolves if get_server_member_discord(&db, &command.user.id).await.is_none() { - return "Not linked with wolves, please use ``/link_wolves`` with your wolves email.".to_string(); + return "Not linked with wolves, please use ``/wolves link`` with your wolves email.".to_string(); } - let username = if let CommandDataOptionValue::String(username) = command - .data - .options - .first() - .expect("Expected username option") - .resolved - .as_ref() - .expect("Expected username object") + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(options), + .. + }) = command.data.options.first() { - username.trim() + options + } else { + return "Please provide sub options".to_string(); + }; + + let username = if let Some(x) = sub_options.first() { + match &x.value { + CommandDataOptionValue::String(username) => username.trim(), + _ => return "Please provide a valid username".to_string(), + } } else { return "Please provide a valid username".to_string(); }; - // insert the username into the database - match add_minecraft(&db, &command.user.id, username).await { - Ok(_) => {} - Err(e) => { - dbg!("{:?}", e); - return format!("Failure to minecraft username {:?}", username); + let java = if let Some(x) = sub_options.get(1) { + match &x.value { + CommandDataOptionValue::Boolean(z) => !z, + _ => true, + } + } else { + true + }; + + let username_mc; + if java { + // insert the username into the database + match add_minecraft(&db, &command.user.id, username).await { + Ok(_) => {} + Err(e) => { + dbg!("{:?}", e); + return format!("Failure to minecraft username {username:?}"); + } + } + username_mc = username.to_string(); + } else { + match get_minecraft_bedrock(username, &config.minecraft_mcprofile).await { + None => { + return format!("No UID found for {username:?}"); + } + Some(x) => { + match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await { + Ok(_) => {} + Err(e) => { + dbg!("{:?}", e); + return format!("Failure to minecraft UID {:?}", &x.floodgateuid); + } + } + + username_mc = x.floodgateuid; + } } } // get a list of servers that the user is a member of if let Ok(servers) = get_servers(&db, &command.user.id).await { for server in servers { - whitelist_update(&vec![username.to_string()], &server.minecraft, &config.discord_token_minecraft).await; + whitelist_update(&vec![(username_mc.to_owned(), java)], &server.minecraft, &config.discord_token_minecraft).await; } } @@ -88,7 +116,52 @@ pub(crate) mod user { WHERE discord = ?1; ", ) - .bind(*user.as_u64() as i64) + .bind(user.get() as i64) + .bind(minecraft) + .fetch_optional(db) + .await + } + + #[derive(Serialize, Deserialize, Debug)] + struct BedrockDetails { + pub gamertag: String, + pub xuid: String, + pub floodgateuid: String, + pub icon: String, + pub gamescore: String, + pub accounttier: String, + pub textureid: String, + pub skin: String, + pub linked: bool, + pub java_uuid: String, + pub java_name: String, + } + + async fn get_minecraft_bedrock(username: &str, api_key: &str) -> Option { + let url = format!("https://mcprofile.io/api/v1/bedrock/gamertag/{username}/"); + match surf::get(url) + .header("x-api-key", api_key) + .header("User-Agent", "UL Computer Society") + .recv_json() + .await + { + Ok(res) => Some(res), + Err(e) => { + dbg!(e); + None + } + } + } + + async fn add_minecraft_bedrock(db: &Pool, user: &UserId, minecraft: &str) -> Result, Error> { + sqlx::query_as::<_, Wolves>( + " + UPDATE wolves + SET minecraft_uid = ?2 + WHERE discord = ?1; + ", + ) + .bind(user.get() as i64) .bind(minecraft) .fetch_optional(db) .await @@ -107,7 +180,7 @@ pub(crate) mod user { ) sub on minecraft.server_discord = sub.server ", ) - .bind(*discord.as_u64() as i64) + .bind(discord.get() as i64) .fetch_all(db) .await } @@ -118,42 +191,40 @@ pub(crate) mod server { use super::*; pub(crate) mod add { - use serenity::model::id::GuildId; + use serenity::{ + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption}, + model::id::GuildId, + }; use sqlx::Error; // this is to managfe the server side of commands related to minecraft use super::*; - use skynet_discord_bot::{is_admin, update_server, Config, Minecraft}; + use skynet_discord_bot::{ + common::minecraft::{update_server, Minecraft}, + Config, + }; - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("minecraft_add").description("Add a minecraft server").create_option(|option| { - option - .name("server_id") - .description("ID of the Minecraft server hosted by the Computer Society") - .kind(CommandOptionType::String) - .required(true) - }) + pub fn register() -> CreateCommand { + CreateCommand::new("minecraft_add") + .description("Add a minecraft server") + .default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) + .add_option( + CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society") + .required(true), + ) } - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - // check if user has high enough permisssions - if let Some(msg) = is_admin(command, ctx).await { - return msg; - } + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { let g_id = match command.guild_id { None => return "Not in a server".to_string(), Some(x) => x, }; - let server_minecraft = if let CommandDataOptionValue::String(id) = command - .data - .options - .first() - .expect("Expected server_id option") - .resolved - .as_ref() - .expect("Expected server_id object") + let server_minecraft = if let Some(CommandDataOption { + value: CommandDataOptionValue::String(id), + .. + }) = command.data.options.first() { - id.to_owned() + id.to_string() } else { return String::from("Expected Server ID"); }; @@ -167,7 +238,7 @@ pub(crate) mod server { match add_server(&db, &g_id, &server_minecraft).await { Ok(_) => {} Err(e) => { - println!("{:?}", e); + println!("{e:?}"); return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft); } } @@ -190,7 +261,7 @@ pub(crate) mod server { VALUES (?1, ?2) ", ) - .bind(*discord.as_u64() as i64) + .bind(discord.get() as i64) .bind(minecraft) .fetch_optional(db) .await @@ -198,19 +269,22 @@ pub(crate) mod server { } pub(crate) mod list { - use serenity::builder::CreateApplicationCommand; - use serenity::client::Context; - use serenity::model::prelude::application_command::ApplicationCommandInteraction; - use skynet_discord_bot::{get_minecraft_config_server, is_admin, server_information, Config, DataBase}; + use serenity::{all::CommandInteraction, builder::CreateCommand, client::Context}; + use skynet_discord_bot::{ + common::{ + database::DataBase, + minecraft::{get_minecraft_config_server, server_information}, + }, + Config, + }; - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("minecraft_list").description("List your minecraft servers") + pub fn register() -> CreateCommand { + CreateCommand::new("minecraft_list") + .default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) + .description("List your minecraft servers") } - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - if let Some(msg) = is_admin(command, ctx).await { - return msg; - } + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { let g_id = match command.guild_id { None => return "Not in a server".to_string(), Some(x) => x, @@ -243,7 +317,7 @@ pub(crate) mod server { ID: {id} Online: {online} Info: {description} - Link: + Link: "#, name = &x.attributes.name, online = !x.attributes.is_suspended, @@ -257,44 +331,37 @@ pub(crate) mod server { } pub(crate) mod delete { - use serenity::builder::CreateApplicationCommand; - use serenity::client::Context; - use serenity::model::application::command::CommandOptionType; - use serenity::model::id::GuildId; - use serenity::model::prelude::application_command::{ApplicationCommandInteraction, CommandDataOptionValue}; - use skynet_discord_bot::{is_admin, DataBase, Minecraft}; + use serenity::{ + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption}, + builder::CreateCommand, + client::Context, + model::id::GuildId, + }; + use skynet_discord_bot::common::{database::DataBase, minecraft::Minecraft}; use sqlx::{Error, Pool, Sqlite}; - pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { - command.name("minecraft_delete").description("Delete a minecraft server").create_option(|option| { - option - .name("server_id") - .description("ID of the Minecraft server hosted by the Computer Society") - .kind(CommandOptionType::String) - .required(true) - }) + pub fn register() -> CreateCommand { + CreateCommand::new("minecraft_delete") + .description("Delete a minecraft server") + .default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) + .add_option( + CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society") + .required(true), + ) } - pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { - // check if user has high enough permisssions - if let Some(msg) = is_admin(command, ctx).await { - return msg; - } + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { let g_id = match command.guild_id { None => return "Not in a server".to_string(), Some(x) => x, }; - let server_minecraft = if let CommandDataOptionValue::String(id) = command - .data - .options - .first() - .expect("Expected server_id option") - .resolved - .as_ref() - .expect("Expected server_id object") + let server_minecraft = if let Some(CommandDataOption { + value: CommandDataOptionValue::String(id), + .. + }) = command.data.options.first() { - id.to_owned() + id.to_string() } else { return String::from("Expected Server ID"); }; @@ -308,7 +375,7 @@ pub(crate) mod server { match server_remove(&db, &g_id, &server_minecraft).await { Ok(_) => {} Err(e) => { - println!("{:?}", e); + println!("{e:?}"); return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft); } } @@ -325,7 +392,7 @@ pub(crate) mod server { WHERE server_discord = ?1 AND server_minecraft = ?2 ", ) - .bind(*discord.as_u64() as i64) + .bind(discord.get() as i64) .bind(minecraft) .fetch_optional(db) .await diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 3acfe81..5815541 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,7 @@ pub mod add_server; pub mod committee; -pub mod link_email; +pub mod count; pub mod minecraft; +pub mod role_adder; +pub mod server_icon; +pub mod wolves; diff --git a/src/commands/role_adder.rs b/src/commands/role_adder.rs new file mode 100644 index 0000000..e60ac83 --- /dev/null +++ b/src/commands/role_adder.rs @@ -0,0 +1,193 @@ +use serenity::client::Context; + +use skynet_discord_bot::common::database::{DataBase, RoleAdder}; +use sqlx::{Error, Pool, Sqlite}; + +pub mod edit { + use super::*; + use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(options), + .. + }) = command.data.options.first() + { + options + } else { + return "Please provide sub options".to_string(); + }; + + let role_a = if let Some(x) = sub_options.first() { + match &x.value { + CommandDataOptionValue::Role(role) => role.to_owned(), + _ => return "Please provide a valid role for ``Role A``".to_string(), + } + } else { + return "Please provide a valid role for ``Role A``".to_string(); + }; + + let role_b = if let Some(x) = sub_options.get(1) { + match &x.value { + CommandDataOptionValue::Role(role) => role.to_owned(), + _ => return "Please provide a valid role for ``Role B``".to_string(), + } + } else { + return "Please provide a valid role for ``Role B``".to_string(); + }; + + let role_c = if let Some(x) = sub_options.get(2) { + match &x.value { + CommandDataOptionValue::Role(role) => role.to_owned(), + _ => return "Please provide a valid role for ``Role C``".to_string(), + } + } else { + return "Please provide a valid role for ``Role C``".to_string(); + }; + + if role_a == role_b { + return "Roles A and B must be different".to_string(); + } + + if (role_c == role_a) || (role_c == role_b) { + return "Role C cannot be same as A or B".to_string(); + } + + let delete = if let Some(x) = sub_options.get(3) { + match &x.value { + CommandDataOptionValue::Boolean(z) => *z, + _ => false, + } + } else { + false + }; + + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let server = command.guild_id.unwrap_or_default(); + let server_data = RoleAdder { + server, + role_a, + role_b, + role_c, + }; + + match add_server(&db, &server_data, delete).await { + Ok(_) => {} + Err(e) => { + println!("{e:?}"); + return format!("Failure to insert into Servers {server_data:?}"); + } + } + + let mut role_a_name = String::new(); + let mut role_b_name = String::new(); + let mut role_c_name = String::new(); + + if let Ok(x) = server.roles(&ctx).await { + if let Some(y) = x.get(&role_a) { + role_a_name = y.to_owned().name; + } + if let Some(y) = x.get(&role_b) { + role_b_name = y.to_owned().name; + } + if let Some(y) = x.get(&role_b) { + role_c_name = y.to_owned().name; + } + } + + if delete { + format!("Removed {role_a_name} + {role_b_name} = {role_c_name}") + } else { + format!("Added {role_a_name} + {role_b_name} = {role_c_name}") + } + } + + async fn add_server(db: &Pool, server: &RoleAdder, delete: bool) -> Result, Error> { + if delete { + sqlx::query_as::<_, RoleAdder>( + " + DELETE FROM roles_adder + WHERE server = ?1 AND role_a = ?2 AND role_b = ?3 AND role_c = ?4 + ", + ) + .bind(server.server.get() as i64) + .bind(server.role_a.get() as i64) + .bind(server.role_b.get() as i64) + .bind(server.role_c.get() as i64) + .fetch_optional(db) + .await + } else { + sqlx::query_as::<_, RoleAdder>( + " + INSERT OR REPLACE INTO roles_adder (server, role_a, role_b, role_c) + VALUES (?1, ?2, ?3, ?4) + ", + ) + .bind(server.server.get() as i64) + .bind(server.role_a.get() as i64) + .bind(server.role_b.get() as i64) + .bind(server.role_c.get() as i64) + .fetch_optional(db) + .await + } + } +} + +// TODO +pub mod list {} + +pub mod tools { + use serenity::{client::Context, model::guild::Member}; + use skynet_discord_bot::common::database::RoleAdder; + use sqlx::{Pool, Sqlite}; + + pub async fn on_role_change(db: &Pool, ctx: &Context, new_data: Member) { + // check if the role changed is part of the oens for this server + if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>( + r#" + SELECT * + FROM roles_adder + WHERE server = ? + "#, + ) + .bind(new_data.guild_id.get() as i64) + .fetch_all(db) + .await + { + let mut roles_add = vec![]; + let mut roles_remove = vec![]; + + for role_adder in role_adders { + // if the user has both A dnd B give them C + if new_data.roles.contains(&role_adder.role_a) && new_data.roles.contains(&role_adder.role_b) && !new_data.roles.contains(&role_adder.role_c) + { + roles_add.push(role_adder.role_c); + } + + // If the suer has C but not A or B remove C + if new_data.roles.contains(&role_adder.role_c) + && (!new_data.roles.contains(&role_adder.role_a) || !new_data.roles.contains(&role_adder.role_b)) + { + roles_remove.push(role_adder.role_c); + } + } + + if !roles_add.is_empty() { + if let Err(e) = new_data.add_roles(&ctx, &roles_add).await { + println!("{e:?}"); + } + } + + if !roles_remove.is_empty() { + if let Err(e) = new_data.remove_roles(&ctx, &roles_remove).await { + println!("{e:?}"); + } + } + } + } +} diff --git a/src/commands/server_icon.rs b/src/commands/server_icon.rs new file mode 100644 index 0000000..0cba1b1 --- /dev/null +++ b/src/commands/server_icon.rs @@ -0,0 +1,226 @@ +use serenity::all::{CommandInteraction, Context}; +use skynet_discord_bot::{ + common::{ + database::DataBase, + server_icon::{get_config_icons, update_icon::update_icon_main, ServerIcons}, + }, + Config, +}; + +use serenity::all::{CommandOptionType, CreateCommand, CreateCommandOption}; + +// commands that server mods are able to use +pub(crate) mod admin { + use super::*; + + // Moderators can force a icon change + pub(crate) mod change { + use super::*; + + pub async fn run(_command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config_global = config_lock.read().await; + + let config_toml = get_config_icons::minimal(); + update_icon_main(ctx, &db, &config_global, &config_toml).await; + + "Changed server Icon".to_string() + } + } +} + +// commands for general users +pub(crate) mod user { + use super::*; + use skynet_discord_bot::common::server_icon::get_config_icons::ConfigTomlLocal; + + pub fn register() -> CreateCommand { + CreateCommand::new("icon") + .description("Commands related to the Server Icon") + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommandGroup, "current", "Information on current items.") + .add_sub_option(CreateCommandOption::new(CommandOptionType::SubCommand, "icon", "Information on current icon.")) + .add_sub_option(CreateCommandOption::new(CommandOptionType::SubCommand, "festival", "Information on current festival.")), + ) + .add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "stats", "How many times the particular icon has been used")) + } + + fn get_logo_url(config_toml: &ConfigTomlLocal, logo_name: &str) -> String { + format!("{}/src/branch/main/{}/{}", &config_toml.source.repo, &config_toml.source.directory, logo_name) + } + + /// Regular users can get teh link to teh current icon + pub(crate) mod current { + use super::*; + + pub(crate) mod icon { + use super::*; + use serenity::all::{CreateAttachment, EditInteractionResponse}; + + use sqlx::{Pool, Sqlite}; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_toml = get_config_icons::minimal(); + + if let Some(logo) = get_current_icon(&db).await { + if let Ok(attachment) = CreateAttachment::path(&logo.path).await { + match command.edit_response(&ctx.http, EditInteractionResponse::new().new_attachment(attachment)).await { + Ok(_) => {} + Err(e) => { + dbg!(e); + } + } + } + + format!("[{}]({})", &logo.name, get_logo_url(&config_toml, &logo.name)) + } else { + "Could not find current icon".to_string() + } + } + + pub async fn get_current_icon(db: &Pool) -> Option { + match sqlx::query_as::<_, ServerIcons>( + " + SELECT * from server_icons ORDER BY id DESC LIMIT 1 + ", + ) + .fetch_one(db) + .await + { + Ok(res) => Some(res), + Err(e) => { + dbg!(e); + None + } + } + } + } + + pub(crate) mod festival { + use serenity::all::{CommandInteraction, Context}; + use skynet_discord_bot::{ + common::server_icon::{get_config_icons, update_icon::get_festival}, + Config, + }; + + // use this to return what current festivals are active? + pub async fn run(_command: &CommandInteraction, ctx: &Context) -> String { + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + let config_toml = get_config_icons::full(&config); + + let response = get_festival(&config_toml).current; + + if response.is_empty() { + "No festival currently active".to_string() + } else { + format!("Festivals active: {}", response.join(", ")) + } + } + } + } + + /// Get the statistics of the icons + pub(crate) mod stats { + use super::*; + use sqlx::{Pool, Sqlite}; + + pub async fn run(_command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_toml = get_config_icons::minimal(); + + let totals = get_totals(&db).await; + + fmt_msg(&config_toml, &totals) + } + + #[derive(Debug, Clone, sqlx::FromRow)] + pub struct CountResult { + pub name: String, + pub times: i64, + } + + async fn get_totals(db: &Pool) -> Vec { + sqlx::query_as::<_, CountResult>( + " + SELECT + DISTINCT name, + COUNT(*) OVER(PARTITION BY name) AS times + FROM server_icons + ", + ) + .fetch_all(db) + .await + .unwrap_or_else(|e| { + dbg!(e); + vec![] + }) + } + + fn fmt_msg(config_toml: &ConfigTomlLocal, totals: &[CountResult]) -> String { + let mut totals_local = totals.to_owned(); + totals_local.sort_by_key(|x| x.times); + totals_local.reverse(); + + // msg can be a max 2000 chars long + let mut limit = 2000 - 3; + + let mut response = vec![]; + for CountResult { + name, + times, + } in &totals_local + { + let current_leading = if times < &10 { + "00" + } else if times < &100 { + "0" + } else { + "" + }; + + let url = get_logo_url(config_toml, name); + + // the `` is so that the numbers will be rendered in monospaced font + // the <> is to suppress the URL embed + let line = format!("``{current_leading}{times}`` [{name}](<{url}>)"); + + let length = line.len() + 1; + + // +3 is to account for the closing fense + if length < (limit + 3) { + response.push(line); + limit -= length; + } else { + break; + } + } + + response.join("\n") + } + } +} diff --git a/src/commands/wolves.rs b/src/commands/wolves.rs new file mode 100644 index 0000000..dc40ad0 --- /dev/null +++ b/src/commands/wolves.rs @@ -0,0 +1,550 @@ +use lettre::{ + message::{header, MultiPart, SinglePart}, + transport::smtp::{self, authentication::Credentials}, + Message, SmtpTransport, Transport, +}; +use maud::html; +use serenity::{ + all::CommandOptionType, + builder::{CreateCommand, CreateCommandOption}, + client::Context, + model::id::UserId, +}; +use skynet_discord_bot::{ + common::database::{DataBase, Wolves, WolvesVerify}, + get_now_iso, random_string, Config, +}; +use sqlx::{Pool, Sqlite}; + +pub mod link { + use super::*; + use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + if get_server_member_discord(&db, &command.user.id).await.is_some() { + return "Already linked".to_string(); + } + + db_pending_clear_expired(&db).await; + + if get_verify_from_db(&db, &command.user.id).await.is_some() { + return "Linking already in process, please check email.".to_string(); + } + + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(options), + .. + }) = command.data.options.first() + { + options + } else { + return "Please provide sub options".to_string(); + }; + + let email = if let Some(x) = sub_options.first() { + match &x.value { + CommandDataOptionValue::String(email) => email.trim(), + _ => return "Please provide a valid email".to_string(), + } + } else { + return "Please provide a valid email".to_string(); + }; + + // check if email exists + let details = match get_server_member_email(&db, email).await { + None => { + let invalid_user = "Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string(); + + let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api)); + + // see if the user actually exists + let id = match wolves.get_member(email).await { + None => { + return invalid_user; + } + Some(x) => x, + }; + + // save teh user id and email to teh db + match save_to_db_user(&db, id, email).await { + Ok(x) => x, + Err(x) => { + dbg!(x); + return "Error: unable to save user to teh database, contact Computer Society".to_string(); + } + }; + + // pull it back out (technically could do it in previous step but more explicit) + match get_server_member_email(&db, email).await { + None => { + return "Error: failed to read user from database.".to_string(); + } + Some(x) => x, + } + } + Some(x) => x, + }; + + if details.discord.is_some() { + return "Email already verified".to_string(); + } + + // generate a auth key + let auth = random_string(20); + match send_mail(&config, &details.email, &auth, &command.user.name) { + Ok(_) => match save_to_db(&db, &details, &auth, &command.user.id).await { + Ok(_) => {} + Err(e) => { + return format!("Unable to save to db {} {e:?}", &details.email); + } + }, + Err(e) => { + return format!("Unable to send mail to {} {e:?}", &details.email); + } + } + + format!("Verification email sent to {email}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.") + } + + pub async fn get_server_member_discord(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, Wolves>( + r#" + SELECT * + FROM wolves + WHERE discord = ? + "#, + ) + .bind(user.get() as i64) + .fetch_one(db) + .await + .ok() + } + + async fn get_server_member_email(db: &Pool, email: &str) -> Option { + sqlx::query_as::<_, Wolves>( + r#" + SELECT * + FROM wolves + WHERE email = ? + "#, + ) + .bind(email) + .fetch_one(db) + .await + .ok() + } + + fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result { + let discord = "https://computer.discord.skynet.ie"; + let sender = format!("UL Computer Society <{}>", &config.mail_user); + + // Create the html we want to send. + let html = html! { + head { + title { "UL Wolves Discord Linker" } + style type="text/css" { + "h2, h4 { font-family: Arial, Helvetica, sans-serif; }" + } + } + div { + h2 { "UL Wolves Discord Linker" } + + h3 { "Link your UL Wolves Account to Discord" } + // Substitute in the name of our recipient. + p { "Hi " (user) "," } + p { + "Please paste this line into Discord (and press enter) to verify your discord account:" + br; + pre { "/wolves verify code: " (auth)} + } + hr; + h3 { "Help & Support" } + p { + "If you have issues please refer to our Computer Society Discord Server:" + br; + a href=(discord) { (discord) } + br; + "UL Computer Society" + } + } + }; + + let body_text = format!( + r#" + UL Wolves Discord Linker + Link your UL Wolves Account to Discord + + Link your Account + + Hi {user}, + + Please paste this line into Discord (and press enter) to verify your Discord account: + /wolves verify code: {auth} + + ------------------------------------------------------------------------- + + Help & Support + + If you have issues please refer to our Computer Society Discord Server: + {discord} + UL Computer Society + "# + ); + + // Build the message. + let email = Message::builder() + .from(sender.parse().unwrap()) + .to(mail.parse().unwrap()) + .subject("Skynet: Link Discord to Wolves.") + .multipart( + // This is composed of two parts. + // also helps not trip spam settings (uneven number of url's + MultiPart::alternative() + .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) + .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), + ) + .expect("failed to build email"); + + let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone()); + + // Open a remote connection to gmail using STARTTLS + let mailer = SmtpTransport::starttls_relay(&config.mail_smtp)?.credentials(creds).build(); + + // Send the email + mailer.send(&email) + } + + pub async fn db_pending_clear_expired(pool: &Pool) -> Option { + sqlx::query_as::<_, WolvesVerify>( + r#" + DELETE + FROM wolves_verify + WHERE date_expiry < ? + "#, + ) + .bind(get_now_iso(true)) + .fetch_one(pool) + .await + .ok() + } + + pub async fn get_verify_from_db(db: &Pool, user: &UserId) -> Option { + sqlx::query_as::<_, WolvesVerify>( + r#" + SELECT * + FROM wolves_verify + WHERE discord = ? + "#, + ) + .bind(user.get() as i64) + .fetch_one(db) + .await + .ok() + } + + async fn save_to_db(db: &Pool, record: &Wolves, auth: &str, user: &UserId) -> Result, sqlx::Error> { + sqlx::query_as::<_, WolvesVerify>( + " + INSERT INTO wolves_verify (email, discord, auth_code, date_expiry) + VALUES (?1, ?2, ?3, ?4) + ", + ) + .bind(record.email.to_owned()) + .bind(user.get() as i64) + .bind(auth.to_owned()) + .bind(get_now_iso(false)) + .fetch_optional(db) + .await + } + + async fn save_to_db_user(db: &Pool, id_wolves: i64, email: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Wolves>( + " + INSERT INTO wolves (id_wolves, email) + VALUES ($1, $2) + ON CONFLICT(id_wolves) DO UPDATE SET email = $2 + ", + ) + .bind(id_wolves) + .bind(email) + .fetch_optional(db) + .await + } +} + +pub mod link_docs { + use super::*; + pub mod users { + use super::*; + use serenity::all::CommandInteraction; + + pub async fn run(_command: &CommandInteraction, _ctx: &Context) -> String { + "https://forgejo.skynet.ie/Skynet/discord-bot/src/branch/main/doc/User.md".to_string() + } + } + + // pub mod committee { + // + // } +} + +pub mod verify { + use super::*; + use crate::commands::wolves::link::{db_pending_clear_expired, get_server_member_discord, get_verify_from_db}; + use serenity::{ + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, + model::user::User, + }; + use skynet_discord_bot::common::{ + database::{get_server_config, ServerMembersWolves, Servers}, + wolves::committees::Committees, + }; + use sqlx::Error; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Database in TypeMap.").clone() + }; + let db = db_lock.read().await; + + // check if user has used /link_wolves + let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { + x + } else { + return "Please use ''/wolves link'' first".to_string(); + }; + + let sub_options = if let Some(CommandDataOption { + value: CommandDataOptionValue::SubCommand(options), + .. + }) = command.data.options.first() + { + options + } else { + return "Please provide sub options".to_string(); + }; + + let code = if let Some(x) = sub_options.first() { + match &x.value { + CommandDataOptionValue::String(y) => y.trim(), + _ => return "Please provide a verification code".to_string(), + } + } else { + return "Please provide a verification code".to_string(); + }; + + db_pending_clear_expired(&db).await; + + if details.auth_code != code { + return "Invalid verification code".to_string(); + } + + match db_pending_clear_successful(&db, &command.user.id).await { + Ok(_) => { + return match set_discord(&db, &command.user.id, &details.email).await { + Ok(_) => { + // get teh right roles for the user + set_server_roles(&db, &command.user, ctx).await; + + // check if they are a committee member, and on that server + set_server_roles_committee(&db, &command.user, ctx).await; + + "Discord username linked to Wolves".to_string() + } + Err(e) => { + println!("{e:?}"); + "Failed to save, please try /link_wolves again".to_string() + } + }; + } + Err(e) => println!("{e:?}"), + } + + "Failed to verify".to_string() + } + + async fn db_pending_clear_successful(pool: &Pool, user: &UserId) -> Result, Error> { + sqlx::query_as::<_, WolvesVerify>( + r#" + DELETE + FROM wolves_verify + WHERE discord = ? + "#, + ) + .bind(user.get() as i64) + .fetch_optional(pool) + .await + } + + async fn set_discord(db: &Pool, discord: &UserId, email: &str) -> Result, Error> { + sqlx::query_as::<_, Wolves>( + " + UPDATE wolves + SET discord = ? + WHERE email = ? + ", + ) + .bind(discord.get() as i64) + .bind(email) + .fetch_optional(db) + .await + } + + async fn set_server_roles(db: &Pool, discord: &User, ctx: &Context) { + if let Ok(servers) = get_servers(db, &discord.id).await { + for server in servers { + if let Ok(member) = server.server.member(&ctx.http, &discord.id).await { + if let Some(config) = get_server_config(db, &server.server).await { + let Servers { + role_past, + role_current, + .. + } = config; + + let mut roles = vec![]; + + if let Some(role) = &role_past { + if !member.roles.contains(role) { + roles.push(role.to_owned()); + } + } + + if !member.roles.contains(&role_current) { + roles.push(role_current.to_owned()); + } + + if let Err(e) = member.add_roles(&ctx, &roles).await { + println!("{e:?}"); + } + } + } + } + } + } + + async fn get_committees_id(db: &Pool, wolves_id: i64) -> Vec { + sqlx::query_as::<_, Committees>( + r#" + SELECT * + FROM committees + WHERE committee LIKE ?1 + "#, + ) + .bind(format!("%{wolves_id}%")) + .fetch_all(db) + .await + .unwrap_or_else(|e| { + dbg!(e); + vec![] + }) + } + + async fn set_server_roles_committee(db: &Pool, discord: &User, ctx: &Context) { + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + if let Some(x) = get_server_member_discord(db, &discord.id).await { + // if they are a member of one or more committees, and in teh committee server then give the teh general committee role + // they will get teh more specific vanity role later + if !get_committees_id(db, x.id_wolves).await.is_empty() { + let server = config.committee_server; + let committee_member = config.committee_role; + + if let Ok(member) = server.member(ctx, &discord.id).await { + member.add_roles(&ctx, &[committee_member]).await.unwrap_or_default(); + } + } + } + } + + async fn get_servers(db: &Pool, discord: &UserId) -> Result, Error> { + sqlx::query_as::<_, ServerMembersWolves>( + " + SELECT * + FROM server_members + JOIN wolves USING (id_wolves) + WHERE discord = ? + ", + ) + .bind(discord.get() as i64) + .fetch_all(db) + .await + } +} + +pub mod unlink { + use serenity::all::{CommandInteraction, Context, UserId}; + use skynet_discord_bot::common::database::{DataBase, Wolves}; + use sqlx::{Pool, Sqlite}; + + pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + let db = db_lock.read().await; + + // dosent matter if there is one or not, it will be removed regardless + delete_link(&db, &command.user.id).await; + + "Discord link removed".to_string() + } + + async fn delete_link(db: &Pool, user: &UserId) { + match sqlx::query_as::<_, Wolves>( + " + UPDATE wolves + SET discord = NULL + WHERE discord = ?1; + ", + ) + .bind(user.get() as i64) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + dbg!(e); + } + } + } +} + +pub fn register() -> CreateCommand { + CreateCommand::new("wolves") + .description("Commands related to UL Wolves") + // link + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "link", "Link your Wolves account to your Discord") + .add_sub_option(CreateCommandOption::new(CommandOptionType::String, "email", "UL Wolves Email").required(true)), + ) + // verify + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "verify", "Verify Wolves Email") + .add_sub_option(CreateCommandOption::new(CommandOptionType::String, "code", "Code from verification email").required(true)), + ) + // unlink + .add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "unlink", "Unlink your Wolves account from your Discord")) + .add_option( + CreateCommandOption::new(CommandOptionType::SubCommand, "link_minecraft", "Link your minecraft account") + .add_sub_option(CreateCommandOption::new(CommandOptionType::String, "minecraft_username", "Your Minecraft username").required(true)) + .add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "bedrock_account", "Is this a Bedrock account?").required(false)), + ) + .add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "docs", "Link to where the documentation can be found.")) +} diff --git a/src/common/database.rs b/src/common/database.rs new file mode 100644 index 0000000..0c18668 --- /dev/null +++ b/src/common/database.rs @@ -0,0 +1,279 @@ +use crate::Config; +use serde::{Deserialize, Serialize}; +use serenity::{ + model::{ + guild, + id::{ChannelId, GuildId, RoleId, UserId}, + }, + prelude::TypeMapKey, +}; +use sqlx::{ + sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow}, + Error, FromRow, Pool, Row, Sqlite, +}; +use std::{str::FromStr, sync::Arc}; +use tokio::sync::RwLock; + +pub struct DataBase; +impl TypeMapKey for DataBase { + type Value = Arc>>; +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ServerMembers { + pub server: GuildId, + pub id_wolves: i64, + pub expiry: String, +} + +impl<'r> FromRow<'r, SqliteRow> for ServerMembers { + fn from_row(row: &'r SqliteRow) -> Result { + let server_tmp: i64 = row.try_get("server")?; + let server = GuildId::from(server_tmp as u64); + + Ok(Self { + server, + id_wolves: row.try_get("id_wolves")?, + expiry: row.try_get("expiry")?, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ServerMembersWolves { + pub server: GuildId, + pub id_wolves: i64, + pub expiry: String, + pub email: String, + pub discord: Option, + pub minecraft: Option, + pub minecraft_uid: Option, +} + +impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves { + fn from_row(row: &'r SqliteRow) -> Result { + let server_tmp: i64 = row.try_get("server")?; + let server = GuildId::from(server_tmp as u64); + + Ok(Self { + server, + id_wolves: row.try_get("id_wolves")?, + expiry: row.try_get("expiry")?, + email: row.try_get("email")?, + discord: get_discord_from_row(row), + minecraft: row.try_get("minecraft")?, + minecraft_uid: row.try_get("minecraft_uid")?, + }) + } +} + +fn get_discord_from_row(row: &SqliteRow) -> Option { + match row.try_get("discord") { + Ok(x) => { + let tmp: i64 = x; + if tmp == 0 { + None + } else { + Some(UserId::from(tmp as u64)) + } + } + _ => None, + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Wolves { + pub id_wolves: i64, + pub email: String, + pub discord: Option, + pub minecraft: Option, +} + +impl<'r> FromRow<'r, SqliteRow> for Wolves { + fn from_row(row: &'r SqliteRow) -> Result { + Ok(Self { + id_wolves: row.try_get("id_wolves")?, + email: row.try_get("email")?, + discord: get_discord_from_row(row), + minecraft: row.try_get("minecraft")?, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct WolvesVerify { + pub email: String, + pub discord: UserId, + pub auth_code: String, + pub date_expiry: String, +} + +impl<'r> FromRow<'r, SqliteRow> for WolvesVerify { + fn from_row(row: &'r SqliteRow) -> Result { + let user_tmp: i64 = row.try_get("discord")?; + let discord = UserId::from(user_tmp as u64); + + Ok(Self { + email: row.try_get("email")?, + discord, + auth_code: row.try_get("auth_code")?, + date_expiry: row.try_get("date_expiry")?, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Servers { + pub server: GuildId, + pub wolves_api: String, + pub wolves_id: i64, + pub role_past: Option, + pub role_current: RoleId, + pub member_past: i64, + pub member_current: i64, + pub bot_channel_id: ChannelId, +} + +impl<'r> FromRow<'r, SqliteRow> for Servers { + fn from_row(row: &'r SqliteRow) -> Result { + let server_tmp: i64 = row.try_get("server")?; + let server = GuildId::from(server_tmp as u64); + let role_past = match row.try_get("role_past") { + Ok(x) => { + let tmp: i64 = x; + if tmp == 0 { + None + } else { + Some(RoleId::from(tmp as u64)) + } + } + _ => None, + }; + let role_current = match row.try_get("role_current") { + Ok(x) => { + let tmp: i64 = x; + RoleId::from(tmp as u64) + } + _ => RoleId::from(0u64), + }; + + let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?; + let bot_channel_id = ChannelId::from(bot_channel_tmp as u64); + + Ok(Self { + server, + wolves_api: row.try_get("wolves_api")?, + wolves_id: row.try_get("wolves_id").unwrap_or(0), + role_past, + role_current, + member_past: row.try_get("member_past")?, + member_current: row.try_get("member_current")?, + bot_channel_id, + }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RoleAdder { + pub server: GuildId, + pub role_a: RoleId, + pub role_b: RoleId, + pub role_c: RoleId, +} + +impl<'r> FromRow<'r, SqliteRow> for RoleAdder { + fn from_row(row: &'r SqliteRow) -> Result { + let server_tmp: i64 = row.try_get("server")?; + let server = GuildId::from(server_tmp as u64); + + Ok(Self { + server, + role_a: get_role_from_row(row, "role_a"), + role_b: get_role_from_row(row, "role_b"), + role_c: get_role_from_row(row, "role_c"), + }) + } +} + +pub(crate) fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId { + let id = match row.try_get(col) { + Ok(x) => { + let tmp: i64 = x; + tmp as u64 + } + _ => 0, + }; + + RoleId::from(id) +} + +pub(crate) fn get_channel_from_row(row: &SqliteRow, col: &str) -> ChannelId { + let id = match row.try_get(col) { + Ok(x) => { + let tmp: i64 = x; + tmp as u64 + } + _ => 0, + }; + + ChannelId::from(id) +} + +pub async fn db_init(config: &Config) -> Result, Error> { + let database = format!("{}/{}", &config.home, &config.database); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with( + SqliteConnectOptions::from_str(&format!("sqlite://{database}"))? + .foreign_keys(true) + .create_if_missing(true), + ) + .await?; + + // migrations are amazing! + sqlx::migrate!("./db/migrations").run(&pool).await?; + + Ok(pool) +} + +pub async fn get_server_config(db: &Pool, server: &GuildId) -> Option { + sqlx::query_as::<_, Servers>( + r#" + SELECT * + FROM servers + WHERE server = ? + "#, + ) + .bind(server.get() as i64) + .fetch_one(db) + .await + .ok() +} + +pub async fn get_server_member(db: &Pool, server: &GuildId, member: &guild::Member) -> Result { + sqlx::query_as::<_, ServerMembersWolves>( + r#" + SELECT * + FROM server_members + JOIN wolves USING (id_wolves) + WHERE server = ? AND discord = ? + "#, + ) + .bind(server.get() as i64) + .bind(member.user.id.get() as i64) + .fetch_one(db) + .await +} + +pub async fn get_server_config_bulk(db: &Pool) -> Vec { + sqlx::query_as::<_, Servers>( + r#" + SELECT * + FROM servers + "#, + ) + .fetch_all(db) + .await + .unwrap_or_default() +} diff --git a/src/common/minecraft.rs b/src/common/minecraft.rs new file mode 100644 index 0000000..60f7820 --- /dev/null +++ b/src/common/minecraft.rs @@ -0,0 +1,171 @@ +use crate::{common::set_roles::normal::get_server_member_bulk, Config}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serenity::model::id::GuildId; +use sqlx::{sqlite::SqliteRow, Error, FromRow, Pool, Row, Sqlite}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Minecraft { + pub discord: GuildId, + pub minecraft: String, +} + +impl<'r> FromRow<'r, SqliteRow> for Minecraft { + fn from_row(row: &'r SqliteRow) -> Result { + let server_tmp: i64 = row.try_get("server_discord")?; + let discord = GuildId::from(server_tmp as u64); + + Ok(Self { + discord, + minecraft: row.try_get("server_minecraft")?, + }) + } +} + +/** +loop through all members of server +get a list of folks with mc accounts that are members +and a list that arent members + */ +pub async fn update_server(server_id: &str, db: &Pool, g_id: &GuildId, config: &Config) { + let mut usernames = vec![]; + for member in get_server_member_bulk(db, g_id).await { + if let Some(x) = member.minecraft { + usernames.push((x, true)); + } + if let Some(x) = member.minecraft_uid { + usernames.push((x, false)); + } + } + if !usernames.is_empty() { + whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await; + } +} + +pub async fn post(url: &str, bearer: &str, data: &T) { + match surf::post(url) + .header("Authorization", bearer) + .header("Content-Type", "application/json") + .header("Accept", "Application/vnd.pterodactyl.v1+json") + .body_json(&data) + { + Ok(req) => { + req.await.ok(); + } + Err(e) => { + dbg!(e); + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ServerDetailsResSub { + pub identifier: String, + pub name: String, + pub description: String, + pub is_suspended: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ServerDetailsRes { + pub attributes: ServerDetailsResSub, +} + +async fn get(url: &str, bearer: &str) -> Option { + match surf::get(url) + .header("Authorization", bearer) + .header("Content-Type", "application/json") + .header("Accept", "Application/vnd.pterodactyl.v1+json") + .recv_json() + .await + { + Ok(res) => Some(res), + Err(e) => { + dbg!(e); + + None + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +struct BodyCommand { + command: String, +} + +#[derive(Deserialize, Serialize, Debug)] +struct BodyDelete { + root: String, + files: Vec, +} + +pub async fn whitelist_wipe(server: &str, token: &str) { + let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}"); + let bearer = format!("Bearer {token}"); + + // delete whitelist + let deletion = BodyDelete { + root: "/".to_string(), + files: vec!["whitelist.json".to_string()], + }; + post(&format!("{url_base}/files/delete"), &bearer, &deletion).await; + + // recreate teh file, passing in the type here so the compiler knows what type of vec it is + post::>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await; + + // reload the whitelist + let data = BodyCommand { + command: "whitelist reload".to_string(), + }; + post(&format!("{url_base}/command"), &bearer, &data).await; +} + +pub async fn server_information(server: &str, token: &str) -> Option { + let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}"); + let bearer = format!("Bearer {token}"); + get::(&format!("{url_base}/"), &bearer).await +} + +pub async fn get_minecraft_config(db: &Pool) -> Vec { + sqlx::query_as::<_, Minecraft>( + r#" + SELECT * + FROM minecraft + "#, + ) + .fetch_all(db) + .await + .unwrap_or_default() +} + +pub async fn get_minecraft_config_server(db: &Pool, g_id: GuildId) -> Vec { + sqlx::query_as::<_, Minecraft>( + r#" + SELECT * + FROM minecraft + WHERE server_discord = ?1 + "#, + ) + .bind(g_id.get() as i64) + .fetch_all(db) + .await + .unwrap_or_default() +} + +pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) { + println!("Update whitelist for {server}"); + let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}"); + let bearer = format!("Bearer {token}"); + + for (name, java) in add { + let data = if *java { + BodyCommand { + command: format!("whitelist add {name}"), + } + } else { + BodyCommand { + command: format!("fwhitelist add {name}"), + } + }; + post(&format!("{url_base}/command"), &bearer, &data).await; + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..9e1745e --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,7 @@ +pub mod database; +pub mod minecraft; +pub mod set_roles; +pub mod wolves; + +pub mod renderer; +pub mod server_icon; diff --git a/src/common/renderer.rs b/src/common/renderer.rs new file mode 100644 index 0000000..97e73a0 --- /dev/null +++ b/src/common/renderer.rs @@ -0,0 +1,194 @@ +// this code is taken from https://github.com/MCorange99/svg2colored-png/tree/main +// I was unable to figure out how to use usvg myself so younked it from here. + +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +// use clap::builder::OsStr; +use color_eyre::{eyre::bail, Result}; +use usvg_text_layout::TreeTextToPath; + +#[derive(Debug, Clone)] +pub struct Args { + pub input: PathBuf, + + /// Output folder where the PNG's will be placed + pub output: PathBuf, + + /// Comma seperated colors that will be used in HEX Eg. 000000,ffffff + /// Can be like an object: black:000000,white:ffffff + pub colors: String, + + /// Width of the generated PNG's + pub width: u32, + + /// Height of the generated PNG's + pub height: u32, +} + +#[derive(Debug, Clone)] +enum ColorType { + Array(Vec), + Object(Vec<(String, String)>), + None, +} + +#[derive(Debug, Clone)] +pub struct Renderer { + fontdb: usvg_text_layout::fontdb::Database, + colors: ColorType, + size: (u32, u32), + pub count: u64, +} + +impl Renderer { + pub fn new(args: &Args) -> Result { + let mut db = usvg_text_layout::fontdb::Database::new(); + db.load_system_fonts(); + + let mut this = Self { + fontdb: db, + colors: ColorType::None, + size: (args.width, args.height), + count: 0, + }; + + let colors = if args.colors.contains(':') { + //? object + let obj = args + .colors + .split(',') + .map(|s| { + let s = s.split(':').collect::>(); + + if s.len() < 2 { + dbg!("Invalid color object, try checking help"); + return None; + } + + Some((s[0].to_string(), s[1].to_string())) + }) + .collect::>>(); + + let mut colors = Vec::new(); + + for c in obj.into_iter().flatten() { + std::fs::create_dir_all(args.output.join(&c.0))?; + + colors.push(c); + } + + ColorType::Object(colors) + } else { + //? list + // let colors = args.colors.split(",").map(|s| { + // s.to_string() + // }) + // .collect::>(); + + let mut colors = Vec::new(); + + for color in args.colors.split(',') { + std::fs::create_dir_all(args.output.join(color))?; + colors.push(color.to_string()) + } + + ColorType::Array(colors) + }; + + this.colors = colors; + Ok(this) + } + + pub fn render(&mut self, fi: &Path, args: &Args) -> Result<()> { + match fi.extension() { + Some(e) if e.to_str() == Some("svg") => {} + Some(_) | None => { + dbg!("Filer {:?} is not of type SVG", fi); + // util::logger::warning(format!("File '{}' is not of SVG type", fi.clone().to_str().unwrap())); + bail!("Failed to render"); + } + }; + + match self.colors.clone() { + ColorType::Array(c) => { + for color in c { + // log::info!("Rendering the color {color:?}"); + let fo = self.get_out_file(fi, &color, args); + self.render_one(fi, &fo, &color)?; + } + } + ColorType::Object(c) => { + for o in c { + // log::info!("Rendering the color {:?}", o); + let fo = self.get_out_file(fi, &o.0, args); + self.render_one(fi, &fo, &o.1)?; + } + } + ColorType::None => unreachable!(), + } + + Ok(()) + } + + fn render_one(&mut self, fi: &Path, fo: &Path, color: &String) -> Result<()> { + if fo.exists() { + dbg!("File {fo:?} exists, skipping"); + return Ok(()); + } + + let svg = self.set_color(&self.get_svg_data(fi)?, color); + + let opt = usvg::Options { + // Get file's absolute directory. + resources_dir: std::fs::canonicalize(fi).ok().and_then(|p| p.parent().map(|p| p.to_path_buf())), + ..Default::default() + }; + + let mut tree = match usvg::Tree::from_data(svg.as_bytes(), &opt) { + Ok(v) => v, + Err(_) => { + dbg!("Failed to parse {fi:?}"); + bail!(""); + } + }; + + tree.convert_text(&self.fontdb); + + let mut pixmap = tiny_skia::Pixmap::new(self.size.0, self.size.1).unwrap(); + + // log::info!("Rendering {fo:?}"); + + //? maybe handle this and possibly throw error if its none + let _ = resvg::render(&tree, usvg::FitTo::Size(self.size.0, self.size.1), tiny_skia::Transform::default(), pixmap.as_mut()); + + pixmap.save_png(fo)?; + self.count += 1; + Ok(()) + } + + #[inline] + fn get_out_file(&mut self, fi: &Path, _sub_folder: &str, args: &Args) -> PathBuf { + let mut fo: std::path::PathBuf = args.output.clone(); + // fo.push(sub_folder); + fo.push(fi.file_name().unwrap_or(OsStr::new("default")).to_str().unwrap_or("default").replace(".svg", "")); + fo.set_extension("png"); + fo + } + + fn set_color(&self, svg: &str, color: &String) -> String { + svg.replace("fill=\"currentColor\"", &format!("fill=\"#{color}\"")) + } + + fn get_svg_data(&self, fi: &Path) -> Result { + match std::fs::read_to_string(fi) { + Ok(d) => Ok(d), + Err(_) => { + dbg!("File {fi:?} does not exist"); + bail!("File {fi:?} does not exist"); + } + } + } +} diff --git a/src/common/server_icon.rs b/src/common/server_icon.rs new file mode 100644 index 0000000..d99f265 --- /dev/null +++ b/src/common/server_icon.rs @@ -0,0 +1,404 @@ +use serde::Deserialize; +use std::{ffi::OsString, fs, path::PathBuf}; + +pub mod get_config_icons { + use super::*; + use crate::Config; + + #[derive(Deserialize)] + pub struct ConfigToml { + pub source: ConfigTomlSource, + pub festivals: Vec, + } + + #[derive(Deserialize)] + pub struct ConfigTomlLocal { + pub source: ConfigTomlSource, + } + #[derive(Deserialize)] + pub struct ConfigTomlRemote { + pub festivals: Vec, + } + + #[derive(Deserialize, Debug)] + pub struct ConfigTomlSource { + pub repo: String, + pub directory: String, + pub file: String, + } + + #[derive(Deserialize, Debug)] + pub struct ConfigTomlFestivals { + pub name: String, + pub all_year: bool, + pub start: ConfigTomlFestivalsTime, + pub end: ConfigTomlFestivalsTime, + } + + #[derive(Deserialize, Debug)] + pub struct ConfigTomlFestivalsTime { + pub day: u32, + pub month: u32, + pub year: i32, + } + pub fn minimal() -> ConfigTomlLocal { + let toml_raw_min = include_str!("../../.server-icons.toml"); + toml::from_str::(toml_raw_min).unwrap_or_else(|e| { + dbg!(e); + ConfigTomlLocal { + source: ConfigTomlSource { + repo: "".to_string(), + directory: "".to_string(), + file: "".to_string(), + }, + } + }) + } + + // since a copy of the festival file is in the repo we just need to get to it + pub fn full(config: &Config) -> ConfigToml { + let config_source = minimal(); + + let file_path = format!("{}/open-governance/{}/{}", &config.home, &config_source.source.directory, &config_source.source.file); + let contents = fs::read_to_string(file_path).unwrap_or_else(|e| { + dbg!(e); + "".to_string() + }); + let festivals = match toml::from_str::(&contents) { + Ok(config_festivals) => config_festivals.festivals, + Err(e) => { + dbg!(e); + vec![] + } + }; + + ConfigToml { + source: config_source.source, + festivals, + } + } +} + +#[derive(Debug)] +pub struct LogoData { + pub name: OsString, + pub path: PathBuf, +} + +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct ServerIcons { + pub id: i64, + pub name: String, + pub path: String, + pub date: String, +} + +pub mod update_icon { + use super::*; + use crate::{ + common::{ + renderer::{Args, Renderer}, + server_icon::get_config_icons::{self, ConfigToml, ConfigTomlLocal}, + }, + get_now_iso, Config, + }; + use chrono::{Datelike, Utc}; + use rand::{rngs::SmallRng, seq::IndexedRandom, SeedableRng}; + use serenity::{ + all::GuildId, + builder::{CreateAttachment, EditGuild}, + client::Context, + }; + use sqlx::{Pool, Sqlite}; + use std::process::Command; + + /// Update the server icon, pulling from open governance. + pub async fn update_icon_main(ctx: &Context, db: &Pool, config_global: &Config, config_toml_local: &ConfigTomlLocal) { + let server = config_global.compsoc_server; + + // clone repo into local folder + clone_repo(config_global, config_toml_local); + + // now the repo has been downloaded/updated we can now access the festivals + let config_toml = get_config_icons::full(config_global); + + // see if there is a current festival + let festival_data = get_festival(&config_toml); + + // get a list of all the graphics files + let logos = get_logos(config_global, &config_toml); + + // filter them so only the current season (if any) are active + let logos_filtered = logos_filter(&festival_data, logos); + + let mut rng = SmallRng::from_os_rng(); + let logo_selected = logos_filtered.choose(&mut rng).unwrap(); + + logo_set(ctx, db, &server, logo_selected).await; + } + + #[derive(Debug)] + pub struct FestivalData { + pub current: Vec, + exclusions: Vec, + } + + pub fn get_festival(config_toml: &ConfigToml) -> FestivalData { + let today = Utc::now(); + let day = today.day(); + let month = today.month(); + let year = today.year(); + + let mut result = FestivalData { + current: vec![], + exclusions: vec![], + }; + + for festival in &config_toml.festivals { + if (day >= festival.start.day && day <= festival.end.day) && (month >= festival.start.month && month <= festival.end.month) { + if festival.start.year == 0 || festival.end.year == 0 || (year >= festival.start.year && year <= festival.end.year) { + result.current.push(festival.name.to_owned()); + } + } else if !festival.all_year { + result.exclusions.push(festival.name.to_owned()); + } + } + + result + } + + fn clone_repo(config: &Config, config_toml: &ConfigTomlLocal) { + let url = &config_toml.source.repo; + let folder = format!("{}/open-governance", &config.home); + + if let Err(e) = Command::new("git") + // clone the repo, gracefully "fails" + .arg("clone") + .arg(url) + .arg(&folder) + .output() + { + dbg!(e); + } + + if let Err(e) = Command::new("git") + // Update the repo + .arg("pull") + .arg("origin") + .arg("main") + .current_dir(&folder) + .output() + { + dbg!(e); + } + + if let Err(e) = Command::new("git") + // Install LFS for the repo + .arg("lfs") + .arg("install") + .current_dir(&folder) + .output() + { + dbg!(e); + } + + if let Err(e) = Command::new("git") + // clone the repo, gracefully "fails" + .arg("lfs") + .arg("pull") + .arg("origin") + .arg("main") + .current_dir(&folder) + .output() + { + dbg!(e); + } + } + + fn get_logos(config: &Config, config_toml: &ConfigToml) -> Vec { + let folder = format!("{}/open-governance/{}", &config.home, &config_toml.source.directory); + let folder_path = PathBuf::from(&folder); + let mut folder_output = folder_path.clone(); + folder_output.push("converted"); + let paths = match fs::read_dir(folder) { + Ok(x) => x, + Err(e) => { + dbg!(e); + return vec![]; + } + }; + + let args = Args { + input: folder_path.clone(), + output: folder_output, + colors: String::from(""), + width: 1024, + height: 1024, + }; + let mut r = match Renderer::new(&args) { + Ok(x) => x, + Err(e) => { + let _ = dbg!(e); + return vec![]; + } + }; + + let mut logos = vec![]; + + for tmp in paths.flatten() { + let path_local = tmp.path().to_owned(); + let path_local2 = tmp.path().to_owned(); + let name = match path_local2.file_name() { + None => { + dbg!(path_local2); + continue; + } + Some(x) => x.to_owned(), + }; + let mut path = tmp.path(); + + if path.is_dir() { + continue; + } + + match tmp.path().extension() { + None => {} + Some(ext) => { + if ext == "svg" { + let mut path_new = path_local.clone(); + path_new.set_extension("png"); + let filename_tmp = path_new.clone(); + let filename = match filename_tmp.file_name() { + None => { + dbg!(filename_tmp); + continue; + } + Some(x) => x, + }; + path_new.pop(); + path_new.push("converted"); + path_new.push(filename); + + // check if exists + if !path_new.exists() { + // convert if it hasnt been converted already + match r.render(&path_local, &args) { + Ok(_) => {} + Err(_e) => { + dbg!("Failed to render {path_local:?}: {}"); + } + } + } + path = path_new; + } + } + }; + + logos.push(LogoData { + name, + path, + }); + + // println!("Name: {}", &tmp.path().display()); + } + + logos + } + + fn logos_filter(festival_data: &FestivalData, existing: Vec) -> Vec { + let mut filtered: Vec = vec![]; + + let allowed_files = vec![".png", ".jpeg", ".gif", ".svg"]; + 'outer: for logo in existing { + let name_lowercase0 = logo.name.to_ascii_lowercase(); + let name_lowercase = name_lowercase0.to_str().unwrap_or_default(); + let mut allowed = false; + for allowed_type in &allowed_files { + if name_lowercase.ends_with(allowed_type) { + allowed = true; + } + } + if !allowed { + continue; + } + + if !festival_data.current.is_empty() { + // if its a current festival filter based on it + for festival in &festival_data.current { + if name_lowercase.contains(festival) { + filtered.push(logo); + continue 'outer; + } + } + } else { + // else filter using the excluded ones + let mut excluded = false; + for festival in &festival_data.exclusions { + if name_lowercase.contains(festival) { + excluded = true; + } + } + + if !excluded { + filtered.push(logo); + } + } + } + + filtered + } + + async fn logo_set(ctx: &Context, db: &Pool, server: &GuildId, logo_selected: &LogoData) { + // add to teh database + if !logo_set_db(db, logo_selected).await { + // something went wrong + return; + } + + if let Some(logo_path) = logo_selected.path.to_str() { + match CreateAttachment::path(logo_path).await { + Ok(icon) => { + // assuming a `guild` has already been bound + let builder = EditGuild::new().icon(Some(&icon)); + if let Err(e) = server.edit(ctx, builder).await { + dbg!(e); + } + } + Err(e) => { + dbg!(e); + } + } + } + } + + async fn logo_set_db(db: &Pool, logo_selected: &LogoData) -> bool { + let name = match logo_selected.name.to_str() { + None => return false, + Some(x) => x, + }; + let path = match logo_selected.path.to_str() { + None => return false, + Some(x) => x, + }; + + match sqlx::query_as::<_, ServerIcons>( + " + INSERT OR REPLACE INTO server_icons (name, date, path) + VALUES (?1, ?2, ?3) + ", + ) + .bind(name) + .bind(get_now_iso(false)) + .bind(path) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + dbg!(e); + return false; + } + } + true + } +} diff --git a/src/common/set_roles.rs b/src/common/set_roles.rs new file mode 100644 index 0000000..6e71992 --- /dev/null +++ b/src/common/set_roles.rs @@ -0,0 +1,537 @@ +pub mod normal { + use crate::{ + common::database::{DataBase, ServerMembersWolves, Servers, Wolves}, + get_now_iso, + }; + use serenity::{ + client::Context, + model::id::{GuildId, RoleId, UserId}, + }; + use sqlx::{Pool, Sqlite}; + + struct RolesChange { + total: i32, + new: i32, + current_add: i32, + current_rem: i32, + } + + pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option], members_changed: &[UserId]) { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Database in TypeMap.").clone() + }; + + let db = db_lock.read().await; + + let Servers { + server, + role_past, + role_current, + .. + } = server; + + let mut roles_set = RolesChange { + total: 0, + new: 0, + current_add: 0, + current_rem: 0, + }; + let mut members = vec![]; + + for member in get_server_member_bulk(&db, server).await { + if let Some(x) = member.discord { + members.push(x); + } + } + let mut members_all = members.len(); + + if let Ok(x) = server.members(ctx, None, None).await { + for member in x { + // members_changed acts as an override to only deal with teh users in it + if !members_changed.is_empty() && !members_changed.contains(&member.user.id) { + continue; + } + + if members.contains(&member.user.id) { + roles_set.total += 1; + + let mut roles = vec![]; + + if let Some(role) = &role_past { + if !member.roles.contains(role) { + roles_set.new += 1; + roles.push(role.to_owned()); + } + } + + if !member.roles.contains(role_current) { + roles_set.current_add += 1; + roles.push(role_current.to_owned()); + } + + if let Err(e) = member.add_roles(ctx, &roles).await { + println!("{e:?}"); + } + } else { + // old and never + + if let Some(role) = &role_past { + if member.roles.contains(role) { + members_all += 1; + } + } + + if member.roles.contains(role_current) { + roles_set.current_rem += 1; + // if theya re not a current member and have the role then remove it + if let Err(e) = member.remove_role(ctx, role_current).await { + println!("{e:?}"); + } + } + } + for role in remove_roles.iter().flatten() { + if let Err(e) = member.remove_role(ctx, role).await { + println!("{e:?}"); + } + } + } + } + + set_server_numbers(&db, server, members_all as i64, members.len() as i64).await; + + // small bit of logging to note changes over time + println!( + "{:?} Total: {} Changes: New: +{}, Current: +{}/-{}", + server.get(), + roles_set.total, + roles_set.new, + roles_set.current_add, + roles_set.current_rem + ); + } + + pub async fn get_server_member_bulk(db: &Pool, server: &GuildId) -> Vec { + sqlx::query_as::<_, ServerMembersWolves>( + r#" + SELECT * + FROM server_members + JOIN wolves USING (id_wolves) + WHERE ( + server = ? + AND discord IS NOT NULL + AND expiry > ? + ) + "#, + ) + .bind(server.get() as i64) + .bind(get_now_iso(true)) + .fetch_all(db) + .await + .unwrap_or_default() + } + + async fn set_server_numbers(db: &Pool, server: &GuildId, past: i64, current: i64) { + match sqlx::query_as::<_, Wolves>( + " + UPDATE servers + SET member_past = ?, member_current = ? + WHERE server = ? + ", + ) + .bind(past) + .bind(current) + .bind(server.get() as i64) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to insert into {}", server.get()); + println!("{e:?}"); + } + } + } +} + +// for updating committee members +pub mod committee { + use crate::{ + common::{ + database::{get_channel_from_row, get_role_from_row, DataBase, Wolves}, + wolves::committees::Committees, + }, + Config, + }; + use serde::{Deserialize, Serialize}; + use serenity::{ + all::EditRole, + builder::CreateChannel, + client::Context, + model::{channel::ChannelType, guild::Member, id::ChannelId, prelude::RoleId}, + }; + use sqlx::{sqlite::SqliteRow, Error, FromRow, Pool, Row, Sqlite}; + use std::collections::HashMap; + + pub async fn check_committee(ctx: &Context) { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config_global = config_lock.read().await; + + let server = config_global.committee_server; + + // because to use it to update a single user we need to pre-get the members of teh server + let mut members = server.members(&ctx, None, None).await.unwrap_or_default(); + + update_committees(&db, ctx, &config_global, &mut members).await; + } + + /** + This function can take a vec of members (or just one) and gives tehm the appropiate roles on teh committee server + */ + pub async fn update_committees(db: &Pool, ctx: &Context, config: &Config, members: &mut Vec) { + let server = config.committee_server; + let committee_member = config.committee_role; + let committees = match get_committees(db).await { + None => { + return; + } + Some(x) => x, + }; + let categories = config.committee_category.clone(); + + // information about the server + let mut roles_db = HashMap::new(); + for role in db_roles_get(db).await { + roles_db.insert( + role.id_wolves, + CommitteeRoles { + id_wolves: role.id_wolves, + id_role: role.id_role, + id_channel: role.id_channel, + name_role: role.name_role, + name_channel: role.name_channel, + // always start at 0 + count: 0, + }, + ); + } + + let mut channels = server.channels(&ctx).await.unwrap_or_default(); + + // a map of users and the roles they are goign to be getting + let mut users_roles = HashMap::new(); + + let mut re_order = false; + // we need to create roles and channels if tehy dont already exist + let mut category_index = 0; + let mut i = 0; + loop { + if i >= committees.len() { + break; + } + let committee = &committees[i]; + + // if a club/soc ever changes their name + if let Some(x) = roles_db.get_mut(&committee.id) { + committee.name_full.clone_into(&mut x.name_role); + committee.name_profile.clone_into(&mut x.name_channel); + } + + // handle new clubs/socs + if let std::collections::hash_map::Entry::Vacant(e) = roles_db.entry(committee.id) { + // create channel + // channel is first as the categories can only contain 50 channels + let channel = match server + .create_channel( + &ctx, + CreateChannel::new(&committee.name_profile) + .kind(ChannelType::Text) + .category(categories[category_index]), + ) + .await + { + Ok(x) => { + println!("Created channel: {}", &committee.name_profile); + + x.id + } + Err(x) => { + let tmp = x.to_string(); + dbg!("Unable to create channel: ", &tmp, &tmp.contains("Maximum number of channels in category reached (50)")); + if x.to_string().contains("Maximum number of channels in category reached (50)") { + category_index += 1; + + continue; + } + ChannelId::new(1) + } + }; + + // create role + let role = match server + .create_role(&ctx, EditRole::new().name(&committee.name_full).hoist(false).mentionable(true)) + .await + { + Ok(x) => x.id, + Err(_) => RoleId::new(1), + }; + + let tmp = CommitteeRoles { + id_wolves: committee.id, + id_role: role, + id_channel: channel, + name_role: committee.name_full.to_owned(), + name_channel: committee.name_profile.to_owned(), + count: 0, + }; + + // save it to the db in case of crash or error + db_role_set(db, &tmp).await; + + // insert it into teh local cache + e.insert(tmp); + + re_order = true; + } + + i += 1; + } + + for committee in &committees { + let r = if let Some(x) = roles_db.get(&committee.id) { + x.id_role + } else { + continue; + }; + + for id_wolves in &committee.committee { + // ID in this is the wolves ID, so we need to get a matching discord ID (if one exists) + if let Some(x) = get_server_member_discord(db, id_wolves).await { + if let Some(member_tmp) = x.discord { + if server.member(ctx, &member_tmp).await.is_ok() { + let values = users_roles.entry(member_tmp).or_insert(vec![]); + values.push(r); + + if let Some(x) = roles_db.get_mut(&committee.id) { + x.count += 1; + } + } + } + } + } + } + + // now we have a map of all users that should get roles time to go through all the folks on teh server + for member in members { + // if member.user.id != 136522490632601600 { + // continue; + // } + // + let roles_current = member.roles(ctx).unwrap_or_default(); + + let roles_required = match users_roles.get(&member.user.id) { + None => { + vec![] + } + Some(x) => x.to_owned(), + }; + + let on_committee = !roles_required.is_empty(); + + let mut roles_rem = vec![]; + let mut roles_add = vec![]; + // get a list of all the roles to remove from someone + + let mut roles_current_id = vec![]; + for role in &roles_current { + roles_current_id.push(role.id.to_owned()); + if !roles_required.contains(&role.id) { + if role.id == committee_member && on_committee { + continue; + } + + roles_rem.push(role.id.to_owned()); + } + } + + let has_committee_role = roles_current_id.contains(&committee_member); + + if on_committee && !has_committee_role { + // if there are committee roles then give the general purporse role + roles_add.push(committee_member); + } + if !on_committee && has_committee_role { + roles_rem.push(committee_member); + } + + if !roles_required.is_empty() { + if let Some(x) = roles_db.get_mut(&0) { + x.count += 1; + } + } + + for role in &roles_required { + if !roles_current_id.contains(role) { + roles_add.push(role.to_owned()); + } + } + + if !roles_rem.is_empty() { + member.remove_roles(&ctx, &roles_rem).await.unwrap_or_default(); + } + + if !roles_add.is_empty() { + // these roles are flavor roles, only there to make folks mentionable + member.add_roles(&ctx, &roles_add).await.unwrap_or_default(); + } + } + + let mut channel_names = vec![]; + let mut positions = vec![]; + for role in roles_db.values() { + // save these to db + db_role_set(db, role).await; + + if re_order { + let channel_id = role.id_channel.to_owned(); + if let Some(channel) = channels.get_mut(&channel_id) { + // record the position of each of teh C&S channels + positions.push(channel.position); + + // pull out teh channel names + channel_names.push((role.name_channel.to_owned(), channel_id)); + } + } + } + + if re_order { + // sort by the position and name + positions.sort(); + channel_names.sort_by_key(|(name, _)| name.to_owned()); + + let mut new_positions = vec![]; + for (i, (_, id)) in channel_names.iter().enumerate() { + new_positions.push((id.to_owned(), positions[i] as u64)); + } + + if !new_positions.is_empty() { + match server.reorder_channels(&ctx, new_positions).await { + Ok(_) => { + println!("Successfully re-orderd the committee category"); + } + Err(e) => { + dbg!("Failed to re-order ", e); + } + } + } + } + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub struct CommitteeRoles { + id_wolves: i64, + pub id_role: RoleId, + pub id_channel: ChannelId, + pub name_role: String, + pub name_channel: String, + pub count: i64, + } + + impl<'r> FromRow<'r, SqliteRow> for CommitteeRoles { + fn from_row(row: &'r SqliteRow) -> Result { + Ok(Self { + id_wolves: row.try_get("id_wolves")?, + id_role: get_role_from_row(row, "id_role"), + id_channel: get_channel_from_row(row, "id_channel"), + name_role: row.try_get("name_role")?, + name_channel: row.try_get("name_channel")?, + count: row.try_get("count")?, + }) + } + } + + async fn db_role_set(db: &Pool, role: &CommitteeRoles) { + // expiry + match sqlx::query_as::<_, CommitteeRoles>( + " + INSERT INTO committee_roles (id_wolves, id_role, id_channel, name_role, name_channel, count) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(id_wolves) DO UPDATE SET name_role = $4, name_channel = $5, count = $6 + ", + ) + .bind(role.id_wolves) + .bind(role.id_role.get() as i64) + .bind(role.id_channel.get() as i64) + .bind(&role.name_role) + .bind(&role.name_channel) + .bind(role.count) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to insert into Wolves {role:?}"); + println!("{e:?}"); + } + } + } + + pub async fn db_roles_get(db: &Pool) -> Vec { + // expiry + sqlx::query_as::<_, CommitteeRoles>( + " + SELECT * + FROM committee_roles + ", + ) + .fetch_all(db) + .await + .unwrap_or_else(|e| { + println!("Failure to get Roles from committee_roles"); + println!("{e:?}"); + vec![] + }) + } + + pub async fn get_committees(db: &Pool) -> Option> { + match sqlx::query_as::<_, Committees>( + r#" + SELECT * + FROM committees + "#, + ) + .fetch_all(db) + .await + { + Ok(x) => Some(x), + Err(e) => { + dbg!(e); + None + } + } + } + + async fn get_server_member_discord(db: &Pool, user: &i64) -> Option { + sqlx::query_as::<_, Wolves>( + r#" + SELECT * + FROM wolves + WHERE id_wolves = ? + "#, + ) + .bind(user) + .fetch_one(db) + .await + .ok() + } +} diff --git a/src/common/wolves.rs b/src/common/wolves.rs new file mode 100644 index 0000000..17305f6 --- /dev/null +++ b/src/common/wolves.rs @@ -0,0 +1,271 @@ +use crate::common::database::Wolves; +use serde::{Deserialize, Serialize}; +use sqlx::{Pool, Sqlite}; + +/** + This file relates to anything that directly interacts with teh wolves API +*/ + +#[derive(Deserialize, Serialize, Debug)] +struct WolvesResultUserMin { + // committee: String, + member_id: String, + // first_name: String, + // last_name: String, + contact_email: String, + // opt_in_email: String, + // student_id: Option, + // note: Option, + // expiry: String, + // requested: String, + // approved: String, + // sitename: String, + // domain: String, +} +async fn add_users_wolves(db: &Pool, user: &WolvesResultUserMin) { + // expiry + match sqlx::query_as::<_, Wolves>( + " + INSERT INTO wolves (id_wolves, email) + VALUES ($1, $2) + ON CONFLICT(id_wolves) DO UPDATE SET email = $2 + ", + ) + .bind(&user.member_id) + .bind(&user.contact_email) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to insert into Wolves {user:?}"); + println!("{e:?}"); + } + } +} + +/** + This is getting data for Clubs and Socs +*/ +pub mod cns { + use crate::{ + common::{ + database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers}, + wolves::{add_users_wolves, WolvesResultUserMin}, + }, + Config, + }; + use serenity::{client::Context, model::id::GuildId}; + use sqlx::{Pool, Sqlite}; + use std::collections::BTreeMap; + + impl From<&wolves_oxidised::WolvesUser> for WolvesResultUserMin { + fn from(value: &wolves_oxidised::WolvesUser) -> Self { + Self { + member_id: value.member_id.to_owned(), + contact_email: value.contact_email.to_owned(), + } + } + } + + pub async fn get_wolves(ctx: &Context) { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Database in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + // set up teh client + let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api)); + + for server_config in get_server_config_bulk(&db).await { + let Servers { + server, + // this is the unique api key for each club/soc + wolves_api, + wolves_id, + .. + } = &server_config; + // dbg!(&server_config); + + let existing_tmp = get_server_member(&db, server).await; + let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::>(); + + // list of users that need to be updated for this server + let mut server_name_tmp = None; + for user in wolves.get_members(wolves_api).await { + // dbg!(&user.committee); + if server_name_tmp.is_none() { + server_name_tmp = Some(user.committee_id); + } + let id = user.member_id.parse::().unwrap_or_default(); + match existing.get(&(id as i64)) { + None => { + // user does not exist already, add everything + add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await; + add_users_server_members(&db, server, &user).await; + } + Some(old) => { + // always update wolves table, in case data has changed + add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await; + if old.expiry != user.expiry { + add_users_server_members(&db, server, &user).await; + } + } + } + } + + if let Some(cs_id) = server_name_tmp { + if &cs_id != wolves_id { + set_server_member(&db, server, cs_id).await; + } + } + } + } + + async fn set_server_member(db: &Pool, server: &GuildId, wolves_id: i64) { + match sqlx::query_as::<_, Servers>( + " + UPDATE servers + SET wolves_id = ? + WHERE server = ? + ", + ) + .bind(wolves_id) + .bind(server.get() as i64) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to set server name {}", server.get()); + println!("{e:?}"); + } + } + } + + async fn get_server_member(db: &Pool, server: &GuildId) -> Vec { + sqlx::query_as::<_, ServerMembersWolves>( + r#" + SELECT * + FROM server_members + JOIN wolves USING (id_wolves) + WHERE ( + server = ? + AND discord IS NOT NULL + ) + "#, + ) + .bind(server.get() as i64) + .fetch_all(db) + .await + .unwrap_or_default() + } + + async fn add_users_server_members(db: &Pool, server: &GuildId, user: &wolves_oxidised::WolvesUser) { + match sqlx::query_as::<_, ServerMembers>( + " + INSERT OR REPLACE INTO server_members (server, id_wolves, expiry) + VALUES (?1, ?2, ?3) + ", + ) + .bind(server.get() as i64) + .bind(&user.member_id) + .bind(&user.expiry) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to insert into ServerMembers {} {:?}", server.get(), user); + println!("{e:?}"); + } + } + } +} + +/** + Get and store the data on C&S committees +*/ +pub mod committees { + use crate::{common::database::DataBase, Config}; + use serenity::client::Context; + use sqlx::{Pool, Sqlite}; + + // Database entry for it + #[derive(Debug, Clone, sqlx::FromRow)] + pub struct Committees { + pub id: i64, + pub name_full: String, + pub name_profile: String, + pub name_plain: String, + pub link: String, + #[sqlx(json)] + pub committee: Vec, + } + + impl From for Committees { + fn from(value: wolves_oxidised::WolvesCNS) -> Self { + Self { + id: value.id, + name_full: value.name_full, + name_profile: value.name_profile, + name_plain: value.name_plain, + link: value.link, + committee: value.committee, + } + } + } + + pub async fn get_cns(ctx: &Context) { + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Database in TypeMap.").clone() + }; + let db = db_lock.read().await; + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api)); + // request data from wolves + for committee_wolves in wolves.get_committees().await { + let committee = Committees::from(committee_wolves); + add_committee(&db, &committee).await; + } + } + + async fn add_committee(db: &Pool, committee: &Committees) { + match sqlx::query_as::<_, Committees>( + " + INSERT INTO committees (id, name_profile, name_full, name_plain, link, committee) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT(id) DO UPDATE SET committee = $6 + ", + ) + .bind(committee.id) + .bind(&committee.name_profile) + .bind(&committee.name_full) + .bind(&committee.name_plain) + .bind(&committee.link) + .bind(serde_json::to_string(&committee.committee).unwrap_or_default()) + .fetch_optional(db) + .await + { + Ok(_) => {} + Err(e) => { + println!("Failure to insert into Committees {committee:?}"); + println!("{e:?}"); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3eda4aa..75c9d78 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,16 @@ +pub mod common; + +use chrono::{Datelike, SecondsFormat, Utc}; use dotenvy::dotenv; -use serde::{Deserialize, Serialize}; +use rand::{distr::Alphanumeric, rng, Rng}; use serenity::{ - model::{ - guild, - id::{ChannelId, GuildId, RoleId}, - }, + model::id::{ChannelId, GuildId, RoleId}, prelude::TypeMapKey, }; - -use crate::set_roles::get_server_member_bulk; -use chrono::{Datelike, SecondsFormat, Utc}; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use serde::de::DeserializeOwned; -use serenity::client::Context; -use serenity::model::id::UserId; -use serenity::model::prelude::application_command::ApplicationCommandInteraction; -use sqlx::{ - sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow}, - Error, FromRow, Pool, Row, Sqlite, -}; -use std::{env, str::FromStr, sync::Arc}; +use std::{env, sync::Arc}; use tokio::sync::RwLock; +#[derive(Debug)] pub struct Config { // manages where teh database is stored pub home: String, @@ -30,6 +19,7 @@ pub struct Config { // tokens for discord and other API's pub discord_token: String, pub discord_token_minecraft: String, + pub minecraft_mcprofile: String, // email settings pub mail_smtp: String, @@ -38,16 +28,21 @@ pub struct Config { // wolves API base for clubs/socs pub wolves_url: String, + // API key for accessing more general resources + pub wolves_api: String, + + // discord server for committee + pub committee_server: GuildId, + pub committee_role: RoleId, + pub committee_category: Vec, + + // items pertaining to compsoc only + pub compsoc_server: GuildId, } impl TypeMapKey for Config { type Value = Arc>; } -pub struct DataBase; -impl TypeMapKey for DataBase { - type Value = Arc>>; -} - pub fn get_config() -> Config { dotenv().ok(); @@ -55,6 +50,7 @@ pub fn get_config() -> Config { let mut config = Config { discord_token: "".to_string(), discord_token_minecraft: "".to_string(), + minecraft_mcprofile: "".to_string(), home: ".".to_string(), database: "database.db".to_string(), @@ -63,6 +59,11 @@ pub fn get_config() -> Config { mail_user: "".to_string(), mail_pass: "".to_string(), wolves_url: "".to_string(), + wolves_api: "".to_string(), + committee_server: GuildId::new(1), + committee_role: RoleId::new(1), + committee_category: vec![], + compsoc_server: GuildId::new(1), }; if let Ok(x) = env::var("DATABASE_HOME") { @@ -78,6 +79,9 @@ pub fn get_config() -> Config { if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") { config.discord_token_minecraft = x.trim().to_string(); } + if let Ok(x) = env::var("MINECRAFT_MCPROFILE_KEY") { + config.minecraft_mcprofile = x.trim().to_string(); + } if let Ok(x) = env::var("EMAIL_SMTP") { config.mail_smtp = x.trim().to_string(); @@ -89,269 +93,40 @@ pub fn get_config() -> Config { config.mail_pass = x.trim().to_string(); } - if let Ok(x) = env::var("WOLVES_URL") { + if let Ok(x) = env::var("WOLVES_URL_BASE") { config.wolves_url = x.trim().to_string(); } + if let Ok(x) = env::var("WOLVES_API") { + config.wolves_api = x.trim().to_string(); + } + + if let Ok(x) = env::var("COMMITTEE_DISCORD") { + if let Ok(x) = x.trim().parse::() { + config.committee_server = GuildId::new(x); + } + } + if let Ok(x) = env::var("COMMITTEE_ROLE") { + if let Ok(x) = x.trim().parse::() { + config.committee_role = RoleId::new(x); + } + } + if let Ok(x) = env::var("COMMITTEE_CATEGORY") { + for part in x.split(',') { + if let Ok(x) = part.trim().parse::() { + config.committee_category.push(ChannelId::new(x)); + } + } + } + + if let Ok(x) = env::var("COMPSOC_DISCORD") { + if let Ok(x) = x.trim().parse::() { + config.compsoc_server = GuildId::new(x); + } + } config } -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ServerMembers { - pub server: GuildId, - pub id_wolves: i64, - pub expiry: String, -} -impl<'r> FromRow<'r, SqliteRow> for ServerMembers { - fn from_row(row: &'r SqliteRow) -> Result { - let server_tmp: i64 = row.try_get("server")?; - let server = GuildId::from(server_tmp as u64); - - Ok(Self { - server, - id_wolves: row.try_get("id_wolves")?, - expiry: row.try_get("expiry")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct ServerMembersWolves { - pub server: GuildId, - pub id_wolves: i64, - pub expiry: String, - pub email: String, - pub discord: Option, - pub minecraft: Option, -} -impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves { - fn from_row(row: &'r SqliteRow) -> Result { - let server_tmp: i64 = row.try_get("server")?; - let server = GuildId::from(server_tmp as u64); - let discord = match row.try_get("discord") { - Ok(x) => { - let tmp: i64 = x; - if tmp == 0 { - None - } else { - Some(UserId::from(tmp as u64)) - } - } - _ => None, - }; - - Ok(Self { - server, - id_wolves: row.try_get("id_wolves")?, - expiry: row.try_get("expiry")?, - email: row.try_get("email")?, - discord, - minecraft: row.try_get("minecraft")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Wolves { - pub id_wolves: i64, - pub email: String, - pub discord: Option, - pub minecraft: Option, -} -impl<'r> FromRow<'r, SqliteRow> for Wolves { - fn from_row(row: &'r SqliteRow) -> Result { - let discord = match row.try_get("discord") { - Ok(x) => { - let tmp: i64 = x; - if tmp == 0 { - None - } else { - Some(UserId::from(tmp as u64)) - } - } - _ => None, - }; - - Ok(Self { - id_wolves: row.try_get("id_wolves")?, - email: row.try_get("email")?, - discord, - minecraft: row.try_get("minecraft")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct WolvesVerify { - pub email: String, - pub discord: UserId, - pub auth_code: String, - pub date_expiry: String, -} -impl<'r> FromRow<'r, SqliteRow> for WolvesVerify { - fn from_row(row: &'r SqliteRow) -> Result { - let user_tmp: i64 = row.try_get("discord")?; - let discord = UserId::from(user_tmp as u64); - - Ok(Self { - email: row.try_get("email")?, - discord, - auth_code: row.try_get("auth_code")?, - date_expiry: row.try_get("date_expiry")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Committee { - pub email: String, - pub discord: UserId, - pub auth_code: String, - pub committee: i64, -} -impl<'r> FromRow<'r, SqliteRow> for Committee { - fn from_row(row: &'r SqliteRow) -> Result { - let user_tmp: i64 = row.try_get("discord")?; - let discord = UserId::from(user_tmp as u64); - - Ok(Self { - email: row.try_get("email")?, - discord, - auth_code: row.try_get("auth_code")?, - committee: row.try_get("committee")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Servers { - pub server: GuildId, - pub wolves_api: String, - pub role_past: Option, - pub role_current: RoleId, - pub member_past: i64, - pub member_current: i64, - pub bot_channel_id: ChannelId, - // these can be removed in teh future with an API update - pub server_name: String, - pub wolves_link: String, -} -impl<'r> FromRow<'r, SqliteRow> for Servers { - fn from_row(row: &'r SqliteRow) -> Result { - let server_tmp: i64 = row.try_get("server")?; - let server = GuildId::from(server_tmp as u64); - let role_past = match row.try_get("role_past") { - Ok(x) => { - let tmp: i64 = x; - if tmp == 0 { - None - } else { - Some(RoleId::from(tmp as u64)) - } - } - _ => None, - }; - let role_current = match row.try_get("role_current") { - Ok(x) => { - let tmp: i64 = x; - RoleId::from(tmp as u64) - } - _ => RoleId::from(0u64), - }; - - let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?; - let bot_channel_id = ChannelId::from(bot_channel_tmp as u64); - - Ok(Self { - server, - wolves_api: row.try_get("wolves_api")?, - role_past, - role_current, - member_past: row.try_get("member_past")?, - member_current: row.try_get("member_current")?, - bot_channel_id, - server_name: row.try_get("server_name")?, - wolves_link: row.try_get("wolves_link")?, - }) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Minecraft { - pub discord: GuildId, - pub minecraft: String, -} -impl<'r> FromRow<'r, SqliteRow> for Minecraft { - fn from_row(row: &'r SqliteRow) -> Result { - let server_tmp: i64 = row.try_get("server_discord")?; - let discord = GuildId::from(server_tmp as u64); - - Ok(Self { - discord, - minecraft: row.try_get("server_minecraft")?, - }) - } -} - -pub async fn db_init(config: &Config) -> Result, Error> { - let database = format!("{}/{}", &config.home, &config.database); - - let pool = SqlitePoolOptions::new() - .max_connections(5) - .connect_with( - SqliteConnectOptions::from_str(&format!("sqlite://{}", database))? - .foreign_keys(true) - .create_if_missing(true), - ) - .await?; - - // migrations are amazing! - sqlx::migrate!("./db/migrations").run(&pool).await?; - - Ok(pool) -} - -pub async fn get_server_config(db: &Pool, server: &GuildId) -> Option { - sqlx::query_as::<_, Servers>( - r#" - SELECT * - FROM servers - WHERE server = ? - "#, - ) - .bind(*server.as_u64() as i64) - .fetch_one(db) - .await - .ok() -} - -pub async fn get_server_member(db: &Pool, server: &GuildId, member: &guild::Member) -> Result { - sqlx::query_as::<_, ServerMembersWolves>( - r#" - SELECT * - FROM server_members - JOIN wolves USING (id_wolves) - WHERE server = ? AND discord = ? - "#, - ) - .bind(*server.as_u64() as i64) - .bind(*member.user.id.as_u64() as i64) - .fetch_one(db) - .await -} - -pub async fn get_server_config_bulk(db: &Pool) -> Vec { - sqlx::query_as::<_, Servers>( - r#" - SELECT * - FROM servers - "#, - ) - .fetch_all(db) - .await - .unwrap_or_default() -} - pub fn get_now_iso(short: bool) -> String { let now = Utc::now(); if short { @@ -362,478 +137,5 @@ pub fn get_now_iso(short: bool) -> String { } pub fn random_string(len: usize) -> String { - thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() -} - -pub mod set_roles { - use super::*; - pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option], members_changed: &[UserId]) { - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Database in TypeMap.").clone() - }; - - let db = db_lock.read().await; - - let Servers { - server, - role_past, - role_current, - .. - } = server; - - let mut roles_set = [0, 0, 0]; - let mut members = vec![]; - - for member in get_server_member_bulk(&db, server).await { - if let Some(x) = member.discord { - members.push(x); - } - } - let mut members_all = members.len(); - - if let Ok(x) = server.members(ctx, None, None).await { - for mut member in x { - // members_changed acts as an override to only deal with teh users in it - if !members_changed.is_empty() && !members_changed.contains(&member.user.id) { - continue; - } - - if members.contains(&member.user.id) { - let mut roles = vec![]; - - if let Some(role) = &role_past { - if !member.roles.contains(role) { - roles_set[0] += 1; - roles.push(role.to_owned()); - } - } - - if !member.roles.contains(role_current) { - roles_set[1] += 1; - roles.push(role_current.to_owned()); - } - - if let Err(e) = member.add_roles(ctx, &roles).await { - println!("{:?}", e); - } - } else { - // old and never - - if let Some(role) = &role_past { - if member.roles.contains(role) { - members_all += 1; - } - } - - if member.roles.contains(role_current) { - roles_set[2] += 1; - // if theya re not a current member and have the role then remove it - if let Err(e) = member.remove_role(ctx, role_current).await { - println!("{:?}", e); - } - } - } - for role in remove_roles.iter().flatten() { - if let Err(e) = member.remove_role(ctx, role).await { - println!("{:?}", e); - } - } - } - } - - set_server_numbers(&db, server, members_all as i64, members.len() as i64).await; - - // small bit of logging to note changes over time - println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]); - } - - pub async fn get_server_member_bulk(db: &Pool, server: &GuildId) -> Vec { - sqlx::query_as::<_, ServerMembersWolves>( - r#" - SELECT * - FROM server_members - JOIN wolves USING (id_wolves) - WHERE ( - server = ? - AND discord IS NOT NULL - AND expiry > ? - ) - "#, - ) - .bind(*server.as_u64() as i64) - .bind(get_now_iso(true)) - .fetch_all(db) - .await - .unwrap_or_default() - } - - async fn set_server_numbers(db: &Pool, server: &GuildId, past: i64, current: i64) { - match sqlx::query_as::<_, Wolves>( - " - UPDATE servers - SET member_past = ?, member_current = ? - WHERE server = ? - ", - ) - .bind(past) - .bind(current) - .bind(*server.as_u64() as i64) - .fetch_optional(db) - .await - { - Ok(_) => {} - Err(e) => { - println!("Failure to insert into {}", server.as_u64()); - println!("{:?}", e); - } - } - } -} - -pub mod get_data { - use super::*; - use crate::set_roles::update_server; - use std::collections::BTreeMap; - - #[derive(Deserialize, Serialize, Debug)] - struct WolvesResultUser { - committee: String, - member_id: String, - first_name: String, - last_name: String, - contact_email: String, - opt_in_email: String, - student_id: Option, - note: Option, - expiry: String, - requested: String, - approved: String, - sitename: String, - domain: String, - } - - #[derive(Deserialize, Serialize, Debug)] - struct WolvesResult { - success: i8, - result: Vec, - } - - #[derive(Deserialize, Serialize, Debug)] - struct WolvesResultLocal { - pub id_wolves: String, - pub email: String, - pub expiry: String, - } - pub async fn get_wolves(ctx: &Context) { - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Database in TypeMap.").clone() - }; - let db = db_lock.read().await; - - let config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Config in TypeMap.").clone() - }; - let config = config_lock.read().await; - - for server_config in get_server_config_bulk(&db).await { - let Servers { - server, - wolves_api, - .. - } = &server_config; - - let existing_tmp = get_server_member(&db, server).await; - let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::>(); - - // list of users that need to be updated for this server - let mut user_to_update = vec![]; - for user in get_wolves_sub(&config, wolves_api).await { - let id = user.member_id.parse::().unwrap_or_default(); - match existing.get(&(id as i64)) { - None => { - // user does not exist already, add everything - add_users_wolves(&db, &user).await; - add_users_server_members(&db, server, &user).await; - } - Some(old) => { - // always update wolves table, in case data has changed - add_users_wolves(&db, &user).await; - if old.expiry != user.expiry { - add_users_server_members(&db, server, &user).await; - - if let Some(discord_id) = old.discord { - user_to_update.push(discord_id); - } - } - } - } - } - - if !user_to_update.is_empty() { - update_server(ctx, &server_config, &[], &user_to_update).await; - } - } - } - - pub async fn get_server_member(db: &Pool, server: &GuildId) -> Vec { - sqlx::query_as::<_, ServerMembersWolves>( - r#" - SELECT * - FROM server_members - JOIN wolves USING (id_wolves) - WHERE ( - server = ? - AND discord IS NOT NULL - ) - "#, - ) - .bind(*server.as_u64() as i64) - .fetch_all(db) - .await - .unwrap_or_default() - } - - async fn get_wolves_sub(config: &Config, wolves_api: &str) -> Vec { - if config.wolves_url.is_empty() { - return vec![]; - } - - // get wolves data - if let Ok(mut res) = surf::post(&config.wolves_url).header("X-AM-Identity", wolves_api).await { - if let Ok(WolvesResult { - success, - result, - }) = res.body_json().await - { - if success != 1 { - return vec![]; - } - - return result; - } - } - - vec![] - } - - async fn add_users_wolves(db: &Pool, user: &WolvesResultUser) { - // expiry - match sqlx::query_as::<_, Wolves>( - " - INSERT INTO wolves (id_wolves, email) - VALUES ($1, $2) - ON CONFLICT(id_wolves) DO UPDATE SET email = $2 - ", - ) - .bind(&user.member_id) - .bind(&user.contact_email) - .fetch_optional(db) - .await - { - Ok(_) => {} - Err(e) => { - println!("Failure to insert into Wolves {:?}", user); - println!("{:?}", e); - } - } - } - async fn add_users_server_members(db: &Pool, server: &GuildId, user: &WolvesResultUser) { - match sqlx::query_as::<_, ServerMembers>( - " - INSERT OR REPLACE INTO server_members (server, id_wolves, expiry) - VALUES (?1, ?2, ?3) - ", - ) - .bind(*server.as_u64() as i64) - .bind(&user.member_id) - .bind(&user.expiry) - .fetch_optional(db) - .await - { - Ok(_) => {} - Err(e) => { - println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user); - println!("{:?}", e); - } - } - } -} - -/** - For any time ye need to check if a user who calls a command has admin privlages -*/ -pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) -> Option { - let mut admin = false; - - let g_id = match command.guild_id { - None => return Some("Not in a server".to_string()), - Some(x) => x, - }; - - let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default(); - - if let Ok(member) = g_id.member(&ctx.http, command.user.id).await { - if let Some(permissions) = member.permissions { - if permissions.administrator() { - admin = true; - } - } - - for role_id in member.roles { - if admin { - break; - } - if let Some(role) = roles_server.get(&role_id) { - if role.permissions.administrator() { - admin = true; - } - } - } - } - if !admin { - Some("Administrator permission required".to_string()) - } else { - None - } -} - -/** -loop through all members of server -get a list of folks with mc accounts that are members -and a list that arent members - */ -pub async fn update_server(server_id: &str, db: &Pool, g_id: &GuildId, config: &Config) { - let mut usernames = vec![]; - for member in get_server_member_bulk(db, g_id).await { - if let Some(x) = member.minecraft { - usernames.push(x); - } - } - if !usernames.is_empty() { - whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await; - } -} - -async fn post(url: &str, bearer: &str, data: &T) { - match surf::post(url) - .header("Authorization", bearer) - .header("Content-Type", "application/json") - .header("Accept", "Application/vnd.pterodactyl.v1+json") - .body_json(&data) - { - Ok(req) => { - req.await.ok(); - } - Err(e) => { - dbg!(e); - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -pub struct ServerDetailsResSub { - pub identifier: String, - pub name: String, - pub description: String, - pub is_suspended: bool, -} -#[derive(Deserialize, Serialize, Debug)] -pub struct ServerDetailsRes { - pub attributes: ServerDetailsResSub, -} - -async fn get(url: &str, bearer: &str) -> Option { - match surf::get(url) - .header("Authorization", bearer) - .header("Content-Type", "application/json") - .header("Accept", "Application/vnd.pterodactyl.v1+json") - .recv_json() - .await - { - Ok(res) => Some(res), - Err(e) => { - dbg!(e); - - None - } - } -} - -#[derive(Deserialize, Serialize, Debug)] -struct BodyCommand { - command: String, -} - -#[derive(Deserialize, Serialize, Debug)] -struct BodyDelete { - root: String, - files: Vec, -} - -pub async fn whitelist_update(add: &Vec, server: &str, token: &str) { - let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); - let bearer = format!("Bearer {token}"); - - for name in add { - let data = BodyCommand { - command: format!("whitelist add {name}"), - }; - post(&format!("{url_base}/command"), &bearer, &data).await; - } -} - -pub async fn whitelist_wipe(server: &str, token: &str) { - let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); - let bearer = format!("Bearer {token}"); - - // delete whitelist - let deletion = BodyDelete { - root: "/".to_string(), - files: vec!["whitelist.json".to_string()], - }; - post(&format!("{url_base}/files/delete"), &bearer, &deletion).await; - - // recreate teh file, passing in the type here so the compiler knows what type of vec it is - post::>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await; - - // reload the whitelist - let data = BodyCommand { - command: "whitelist reload".to_string(), - }; - post(&format!("{url_base}/command"), &bearer, &data).await; -} - -pub async fn server_information(server: &str, token: &str) -> Option { - let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}"); - let bearer = format!("Bearer {token}"); - get::(&format!("{url_base}/"), &bearer).await -} - -pub async fn get_minecraft_config(db: &Pool) -> Vec { - sqlx::query_as::<_, Minecraft>( - r#" - SELECT * - FROM minecraft - "#, - ) - .fetch_all(db) - .await - .unwrap_or_default() -} - -pub async fn get_minecraft_config_server(db: &Pool, g_id: GuildId) -> Vec { - sqlx::query_as::<_, Minecraft>( - r#" - SELECT * - FROM minecraft - WHERE server_discord = ?1 - "#, - ) - .bind(*g_id.as_u64() as i64) - .fetch_all(db) - .await - .unwrap_or_default() + rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() } diff --git a/src/main.rs b/src/main.rs index f1c70f3..bbf389f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,69 @@ pub mod commands; +use crate::commands::role_adder::tools::on_role_change; use serenity::{ + all::{Command, CommandDataOptionValue, CreateMessage, EditInteractionResponse, Interaction}, async_trait, client::{Context, EventHandler}, + gateway::{ActivityData, ChunkGuildFilter}, model::{ - application::{command::Command, interaction::Interaction}, + event::GuildMemberUpdateEvent, gateway::{GatewayIntents, Ready}, - guild, - prelude::Activity, + guild::Member, + id::GuildId, user::OnlineStatus, }, Client, }; +use skynet_discord_bot::{ + common::{ + database::{db_init, get_server_config, get_server_member, DataBase}, + set_roles::committee::update_committees, + wolves::committees::Committees, + }, + get_config, Config, +}; +use sqlx::{Pool, Sqlite}; use std::sync::Arc; - -use skynet_discord_bot::{db_init, get_config, get_server_config, get_server_member, Config, DataBase}; use tokio::sync::RwLock; +// Need To Define The Stuct (Committed To Bump The Bot) struct Handler; #[async_trait] impl EventHandler for Handler { - async fn guild_member_addition(&self, ctx: Context, mut new_member: guild::Member) { + // this caches members of all servers teh bot is in + async fn cache_ready(&self, ctx: Context, guilds: Vec) { + for guild in guilds { + ctx.shard.chunk_guild(guild, Some(2000), false, ChunkGuildFilter::None, None); + } + println!("Cache built successfully!"); + } + + // handles previously linked accounts joining the server + async fn guild_member_addition(&self, ctx: Context, new_member: Member) { let db_lock = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Config in TypeMap.").clone() }; let db = db_lock.read().await; - let config = match get_server_config(&db, &new_member.guild_id).await { + + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config_global = config_lock.read().await; + + // committee server takes priority + let committee_server = config_global.committee_server; + if new_member.guild_id.get() == committee_server.get() { + let mut member = vec![new_member.clone()]; + update_committees(&db, &ctx, &config_global, &mut member).await; + return; + } + + let config_server = match get_server_config(&db, &new_member.guild_id).await { None => return, Some(x) => x, }; @@ -36,92 +71,216 @@ impl EventHandler for Handler { if get_server_member(&db, &new_member.guild_id, &new_member).await.is_ok() { let mut roles = vec![]; - if let Some(role) = &config.role_past { + if let Some(role) = &config_server.role_past { if !new_member.roles.contains(role) { roles.push(role.to_owned()); } } - if !new_member.roles.contains(&config.role_current) { - roles.push(config.role_current.to_owned()); + if !new_member.roles.contains(&config_server.role_current) { + roles.push(config_server.role_current.to_owned()); } if let Err(e) = new_member.add_roles(&ctx, &roles).await { - println!("{:?}", e); + println!("{e:?}"); } } else { - let msg = format!( - r#" + let tmp = get_committee(&db, config_server.wolves_id).await; + if !tmp.is_empty() { + let committee = &tmp[0]; + let msg = format!( + r#" Welcome {} to the {} server! -Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/link_wolves`` to get full access. +Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/wolves link email_here`` with the email associated with your wolves account, to get full access. "#, - new_member.display_name(), - &config.server_name, - &config.wolves_link, - &config.server, - &config.bot_channel_id - ); + new_member.display_name(), + committee.name_full, + committee.link, + &config_server.server, + &config_server.bot_channel_id + ); - if let Err(err) = new_member.user.direct_message(&ctx, |m| m.content(&msg)).await { - dbg!(err); + if let Err(err) = new_member.user.direct_message(&ctx, CreateMessage::new().content(&msg)).await { + dbg!(err); + } } } } + // handles role updates + async fn guild_member_update(&self, ctx: Context, _old_data: Option, new_data: Option, _: GuildMemberUpdateEvent) { + // get config/db + let db_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + let db = db_lock.read().await; + + // check if the role changed is part of the oens for this server + if let Some(x) = new_data { + on_role_change(&db, &ctx, x).await; + } + } + async fn ready(&self, ctx: Context, ready: Ready) { println!("[Main] {} is connected!", ready.user.name); - ctx.set_presence(Some(Activity::playing("with humanity's fate")), OnlineStatus::Online).await; + ctx.set_presence(Some(ActivityData::playing("with humanity's fate")), OnlineStatus::Online); - match Command::set_global_application_commands(&ctx.http, |commands| { - commands - .create_application_command(|command| commands::add_server::register(command)) - .create_application_command(|command| commands::link_email::link::register(command)) - .create_application_command(|command| commands::link_email::verify::register(command)) - .create_application_command(|command| commands::minecraft::server::add::register(command)) - .create_application_command(|command| commands::minecraft::server::list::register(command)) - .create_application_command(|command| commands::minecraft::server::delete::register(command)) - .create_application_command(|command| commands::minecraft::user::add::register(command)) - // for committee server, temp - .create_application_command(|command| commands::committee::link::register(command)) - .create_application_command(|command| commands::committee::verify::register(command)) - }) + let config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + let config = config_lock.read().await; + + match Command::set_global_commands( + &ctx.http, + vec![ + commands::wolves::register(), + commands::committee::register(), + commands::minecraft::server::add::register(), + commands::minecraft::server::list::register(), + commands::minecraft::server::delete::register(), + ], + ) .await { Ok(_) => {} Err(e) => { - println!("{:?}", e) + println!("{e:?}") + } + } + + // Inter-Committee server + match config.committee_server.set_commands(&ctx.http, vec![commands::count::committee::register()]).await { + Ok(_) => {} + Err(e) => { + println!("{e:?}") + } + } + + // compsoc Server + match config + .compsoc_server + .set_commands( + &ctx.http, + vec![ + // commands just for the compsoc server + commands::count::servers::register(), + commands::server_icon::user::register(), + ], + ) + .await + { + Ok(_) => {} + Err(e) => { + println!("{e:?}") } } } async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(command) = interaction { + if let Interaction::Command(command) = interaction { let _ = command.defer_ephemeral(&ctx.http).await; //println!("Received command interaction: {:#?}", command); let content = match command.data.name.as_str() { // user commands - "link_wolves" => commands::link_email::link::run(&command, &ctx).await, - "verify" => commands::link_email::verify::run(&command, &ctx).await, - "link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await, + "wolves" => match command.data.options.first() { + None => "Invalid Command".to_string(), + Some(x) => match x.name.as_str() { + "link" => commands::wolves::link::run(&command, &ctx).await, + "verify" => commands::wolves::verify::run(&command, &ctx).await, + "unlink" => commands::wolves::unlink::run(&command, &ctx).await, + "link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await, + "docs" => commands::wolves::link_docs::users::run(&command, &ctx).await, + // "link" => commands::count::servers::run(&command, &ctx).await, + &_ => format!("not implemented :( wolves {}", x.name.as_str()), + }, + }, + // admin commands - "add" => commands::add_server::run(&command, &ctx).await, + "committee" => match command.data.options.first() { + None => "Invalid Command".to_string(), + Some(x) => match x.name.as_str() { + "add" => commands::add_server::run(&command, &ctx).await, + "roles_adder" => commands::role_adder::edit::run(&command, &ctx).await, + "icon" => match &x.value { + CommandDataOptionValue::SubCommandGroup(y) => match y.first() { + None => "error".to_string(), + Some(z) => match z.name.as_str() { + "change" => commands::server_icon::admin::change::run(&command, &ctx).await, + &_ => format!("not implemented :( count {}", x.name.as_str()), + }, + }, + _ => { + format!("not implemented :( committee {}", x.name.as_str()) + } + }, + // TODO: move teh minecraft commands in here as a subgroup + // "link" => commands::count::servers::run(&command, &ctx).await, + &_ => format!("not implemented :( committee {}", x.name.as_str()), + }, + }, "minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await, "minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await, "minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await, - // for teh committee server, temporary - "link_committee" => commands::committee::link::run(&command, &ctx).await, - "verify_committee" => commands::committee::verify::run(&command, &ctx).await, - _ => "not implemented :(".to_string(), + // sub command + "count" => match command.data.options.first() { + None => "Invalid Command".to_string(), + Some(x) => match x.name.as_str() { + "committee" => commands::count::committee::run(&command, &ctx).await, + "servers" => commands::count::servers::run(&command, &ctx).await, + &_ => format!("not implemented :( count {}", x.name.as_str()), + }, + }, + + "icon" => match command.data.options.first() { + None => "Invalid Command".to_string(), + Some(x) => match x.name.as_str() { + "current" => { + let result = match &x.value { + CommandDataOptionValue::SubCommandGroup(y) => match y.first() { + None => "error".to_string(), + Some(z) => match z.name.as_str() { + "icon" => commands::server_icon::user::current::icon::run(&command, &ctx).await, + "festival" => commands::server_icon::user::current::festival::run(&command, &ctx).await, + &_ => format!("not implemented :( count {}", x.name.as_str()), + }, + }, + &_ => format!("not implemented :( {}", command.data.name.as_str()), + }; + + result + } + "stats" => commands::server_icon::user::stats::run(&command, &ctx).await, + &_ => format!("not implemented :( count {}", x.name.as_str()), + }, + }, + _ => format!("not implemented :( {}", command.data.name.as_str()), }; - if let Err(why) = command.edit_original_interaction_response(&ctx.http, |response| response.content(content)).await { - println!("Cannot respond to slash command: {}", why); + if let Err(why) = command.edit_response(&ctx.http, EditInteractionResponse::new().content(content)).await { + println!("Cannot respond to slash command: {why}"); } } } } +async fn get_committee(db: &Pool, wolves_id: i64) -> Vec { + sqlx::query_as::<_, Committees>( + r#" + SELECT * + FROM committees + WHERE id = ? + "#, + ) + .bind(wolves_id) + .fetch_all(db) + .await + .unwrap_or_default() +} + #[tokio::main] async fn main() { let config = get_config(); @@ -137,7 +296,8 @@ async fn main() { let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; // Build our client. let mut client = Client::builder(&config.discord_token, intents) - .event_handler(Handler {}) + .event_handler(Handler) + .cache_settings(serenity::cache::Settings::default()) .await .expect("Error creating client"); @@ -153,6 +313,6 @@ async fn main() { // Shards will automatically attempt to reconnect, and will perform // exponential backoff until it reconnects. if let Err(why) = client.start().await { - println!("Client error: {:?}", why); + println!("Client error: {why:?}"); } }