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/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..a338a00 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Fix typos +7e90f451965b0edbd331765ad295a02f31d2bf24 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/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..3b409e9 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,2 @@ +[formatting] +column_width = 120 diff --git a/Cargo.lock b/Cargo.lock index 47a42f9..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" @@ -110,6 +110,12 @@ version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" 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" @@ -383,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" @@ -449,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" @@ -637,6 +682,12 @@ 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" @@ -790,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" @@ -805,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" @@ -815,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" @@ -849,6 +925,27 @@ 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" @@ -1063,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" @@ -1586,16 +1693,6 @@ dependencies = [ "syn 2.0.89", ] -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "1.0.3" @@ -1617,6 +1714,18 @@ dependencies = [ "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" @@ -1677,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" @@ -1686,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" @@ -1718,7 +1851,7 @@ dependencies = [ "futures-util", "hostname", "httpdate", - "idna 1.0.3", + "idna", "mime", "native-tls", "nom", @@ -1843,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" @@ -1872,6 +2014,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2031,6 +2174,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "owo-colors" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec" + [[package]] name = "parking" version = "2.2.1" @@ -2075,6 +2224,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.7" @@ -2145,6 +2300,19 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.7.4" @@ -2339,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" @@ -2435,6 +2609,34 @@ dependencies = [ "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]] name = "ring" version = "0.17.8" @@ -2450,6 +2652,34 @@ dependencies = [ "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" @@ -2582,6 +2812,22 @@ dependencies = [ "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]] name = "ryu" version = "1.0.18" @@ -2713,6 +2959,15 @@ dependencies = [ "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]] name = "serde_urlencoded" version = "0.7.1" @@ -2806,6 +3061,15 @@ 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" @@ -2831,21 +3095,49 @@ 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.9.0", + "resvg", "serde", "serde_json", "serenity", "sqlx", "surf", + "tiny-skia", "tokio", + "toml", + "usvg", + "usvg-text-layout", "wolves_oxidised", ] @@ -3180,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" @@ -3220,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" @@ -3363,6 +3694,16 @@ dependencies = [ "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]] name = "time" version = "0.2.27" @@ -3432,6 +3773,31 @@ 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" @@ -3581,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" @@ -3617,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]] @@ -3629,12 +4047,29 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ttf-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633" + [[package]] name = "tungstenite" version = "0.21.0" @@ -3683,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" @@ -3704,6 +4157,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + [[package]] name = "universal-hash" version = "0.4.0" @@ -3722,16 +4187,49 @@ 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 = "usvg" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" version = "0.7.6" @@ -3750,6 +4248,12 @@ 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" @@ -3915,6 +4419,12 @@ 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" @@ -4144,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" @@ -4186,6 +4705,12 @@ 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" diff --git a/Cargo.toml b/Cargo.toml index d999267..832eb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,19 +4,21 @@ 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" - [[bin]] name = "update_committee" [[bin]] name = "update_minecraft" +[[bin]] +name = "update_server-icon" + +[[bin]] +name = "cleanup_committee" + [dependencies] # discord library serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } @@ -32,7 +34,7 @@ surf = "2.3" dotenvy = "0.15" # For sqlite -sqlx = { version = "0.8", 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 @@ -45,4 +47,13 @@ chrono = "0.4" lettre = "0.11" maud = "0.27" -serde = "1.0" \ 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 01543e6..1f49dc1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Skynet Discord Bot 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 is hosted by the Computer Society on Skynet (computer cluster). +Skynet (bot) is hosted by the Computer Society on Skynet (computer cluster). ## Documentation We have split up the documentation into different segments depending on who the user is. 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/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 0a37869..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,16 +21,33 @@ nixpkgs, utils, naersk, + nixpkgs-mozilla, }: utils.lib.eachDefaultSystem ( system: let overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); - pkgs = (import nixpkgs) {inherit system;}; - naersk' = pkgs.callPackage naersk {}; + 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 ]; @@ -37,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 { @@ -63,9 +88,9 @@ # `nix develop` devShell = pkgs.mkShell { - nativeBuildInputs = with pkgs; [rustup rustPlatform.bindgenHook pkg-config openssl]; + nativeBuildInputs = with pkgs; [rustup rustPlatform.bindgenHook]; # libraries here - buildInputs = [ ]; + buildInputs = buildInputs; RUSTC_VERSION = overrides.toolchain.channel; # https://github.com/rust-lang/rust-bindgen#environment-variables shellHook = '' @@ -97,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}" @@ -127,14 +154,17 @@ # modify these scripts = { - # every 20 min + # 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" = "*:15:00"; + "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}" = { @@ -194,6 +224,7 @@ after = ["network-online.target"]; wants = []; environment = environment_config; + path = with pkgs; [ git git-lfs ]; serviceConfig = { User = "${cfg.user}"; 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..141af89 --- /dev/null +++ b/src/bin/cleanup_committee.rs @@ -0,0 +1,137 @@ +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 have been set up accidentally +/// DO NOT run this locally unless you have a fresh copy of the 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(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 = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + 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 channel we 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 channel we 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 index d2026c0..d90cbac 100644 --- a/src/bin/update_committee.rs +++ b/src/bin/update_committee.rs @@ -1,12 +1,17 @@ use serenity::{ + all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent}, async_trait, client::{Context, EventHandler}, - model::gateway::{GatewayIntents, Ready}, + model::gateway::GatewayIntents, Client, }; -use skynet_discord_bot::common::database::{db_init, DataBase}; -use skynet_discord_bot::common::set_roles::committee; -use skynet_discord_bot::{get_config, Config}; +use skynet_discord_bot::{ + common::{ + database::{db_init, DataBase}, + set_roles::committee, + }, + get_config, Config, +}; use std::{process, sync::Arc}; use tokio::sync::RwLock; @@ -23,6 +28,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"); @@ -30,25 +36,39 @@ async fn main() { let mut data = client.data.write().await; data.insert::(Arc::new(RwLock::new(config))); - data.insert::(Arc::new(RwLock::new(db))); + data.insert::(Arc::new(db)); } if let Err(why) = client.start().await { - println!("Client error: {:?}", why); + 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); + 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; - // u[date committee server - committee::check_committee(Arc::clone(&ctx)).await; + let server = config_global.committee_server; - // finish up - process::exit(0); + 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 2d36892..fe4138f 100644 --- a/src/bin/update_data.rs +++ b/src/bin/update_data.rs @@ -4,10 +4,13 @@ use serenity::{ model::gateway::{GatewayIntents, Ready}, Client, }; -use skynet_discord_bot::common::database::{db_init, DataBase}; -use skynet_discord_bot::common::wolves::cns::get_wolves; -use skynet_discord_bot::common::wolves::committees::get_cns; -use skynet_discord_bot::{get_config, Config}; +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; @@ -27,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"); @@ -34,11 +38,11 @@ async fn main() { let mut data = client.data.write().await; data.insert::(Arc::new(RwLock::new(config))); - data.insert::(Arc::new(RwLock::new(db))); + data.insert::(Arc::new(db)); } if let Err(why) = client.start().await { - println!("Client error: {:?}", why); + println!("Client error: {why:?}"); } } diff --git a/src/bin/update_minecraft.rs b/src/bin/update_minecraft.rs index f5d3634..f7c24e0 100644 --- a/src/bin/update_minecraft.rs +++ b/src/bin/update_minecraft.rs @@ -1,6 +1,10 @@ -use skynet_discord_bot::common::database::db_init; -use skynet_discord_bot::common::minecraft::{get_minecraft_config, update_server, whitelist_wipe}; -use skynet_discord_bot::get_config; +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..c4f9eca --- /dev/null +++ b/src/bin/update_server-icon.rs @@ -0,0 +1,71 @@ +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(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 = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Config in TypeMap.").clone() + }; + + 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 3617d14..3150bcf 100644 --- a/src/bin/update_users.rs +++ b/src/bin/update_users.rs @@ -1,13 +1,24 @@ use serenity::{ + all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent}, async_trait, client::{Context, EventHandler}, - model::gateway::{GatewayIntents, Ready}, + model::gateway::GatewayIntents, Client, }; -use skynet_discord_bot::common::database::{db_init, get_server_config_bulk, DataBase}; -use skynet_discord_bot::common::set_roles::normal; -use skynet_discord_bot::{get_config, Config}; -use std::{process, sync::Arc}; +use skynet_discord_bot::{ + common::{ + database::{db_init, get_server_config_bulk, DataBase}, + set_roles::normal, + }, + get_config, Config, +}; +use std::{ + process, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; use tokio::sync::RwLock; #[tokio::main] @@ -22,7 +33,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"); @@ -30,38 +45,51 @@ async fn main() { let mut data = client.data.write().await; data.insert::(Arc::new(RwLock::new(config))); - data.insert::(Arc::new(RwLock::new(db))); + data.insert::(Arc::new(db)); } 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)); + } - // this goes into each server and sets roles for each wolves member - check_bulk(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 check_bulk(ctx: Arc) { - let db_lock = { +async fn check_bulk(ctx: &Context) { + let db = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Config in TypeMap.").clone() }; - let db = db_lock.read().await; - for server_config in get_server_config_bulk(&db).await { - normal::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 387bd9e..eaf0971 100644 --- a/src/commands/add_server.rs +++ b/src/commands/add_server.rs @@ -1,8 +1,12 @@ -use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; -use serenity::client::Context; -use skynet_discord_bot::common::database::{get_server_config, DataBase, Servers}; -use skynet_discord_bot::common::set_roles::normal::update_server; -use skynet_discord_bot::common::wolves::cns::get_wolves; +use serenity::{ + all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, + client::Context, +}; +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: &CommandInteraction, ctx: &Context) -> String { @@ -52,11 +56,10 @@ pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { return "Please provide a valid channel for ``Bot Channel``".to_string(); }; - let db_lock = { + let db = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() }; - let db = db_lock.read().await; let server_data = Servers { server: command.guild_id.unwrap_or_default(), @@ -72,8 +75,8 @@ pub async fn run(command: &CommandInteraction, ctx: &Context) -> 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:?}"); } } @@ -98,7 +101,7 @@ async fn add_server(db: &Pool, ctx: &Context, server: &Servers) -> Resul .fetch_optional(db) .await; - // if the entry does not exist already tehn do a user update + // if the entry does not exist already then do a user update let (update, current_remove, current_role, past_remove, past_role) = match &existing { None => (true, false, None, false, None), Some(x) => { diff --git a/src/commands/committee.rs b/src/commands/committee.rs index 4813c6a..944fbc0 100644 --- a/src/commands/committee.rs +++ b/src/commands/committee.rs @@ -20,4 +20,8 @@ pub fn register() -> CreateCommand { .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)), ) + .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.")), + ) } diff --git a/src/commands/count.rs b/src/commands/count.rs index 678c9e0..41b9d81 100644 --- a/src/commands/count.rs +++ b/src/commands/count.rs @@ -5,8 +5,7 @@ pub mod committee { use serenity::all::{ CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, }; - use skynet_discord_bot::common::database::DataBase; - use skynet_discord_bot::common::set_roles::committee::db_roles_get; + 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 { @@ -28,11 +27,10 @@ pub mod committee { false }; - let db_lock = { + let db = { 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 @@ -53,7 +51,7 @@ pub mod committee { for (count, name) in cs { let leading = if count < 10 { " " } else { "" }; - let line = format!("{}{} {}", leading, count, name); + let line = format!("{leading}{count} {name}"); let length = line.len() + 1; @@ -85,22 +83,27 @@ 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}; - use skynet_discord_bot::common::set_roles::committee::get_committees; - use skynet_discord_bot::get_now_iso; + 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 db = { 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(); - for committee in get_committees(&db).await { - committees.insert(committee.id, committee.to_owned()); + if let Some(x) = get_committees(&db).await { + for committee in x { + committees.insert(committee.id, committee.to_owned()); + } } let mut cs = vec![]; @@ -143,11 +146,11 @@ pub mod servers { "" }; - let line = format!("{}{} {}{} {}", current_leading, current, past_leading, past, name); + let line = format!("{current_leading}{current} {past_leading}{past} {name}"); let length = line.len() + 1; - // +3 is to account for the closing fense + // +3 is to account for the closing fence if length < (limit + 3) { response.push(line); limit -= length; diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs index ca395c6..5098f11 100644 --- a/src/commands/minecraft.rs +++ b/src/commands/minecraft.rs @@ -9,19 +9,24 @@ pub(crate) mod user { use super::*; use crate::commands::wolves::link::get_server_member_discord; use serde::{Deserialize, Serialize}; - use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; - use serenity::model::id::UserId; - use skynet_discord_bot::common::database::Wolves; - use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft}; - use skynet_discord_bot::Config; + 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 async fn run(command: &CommandInteraction, ctx: &Context) -> String { - let db_lock = { + let db = { 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; @@ -69,14 +74,14 @@ pub(crate) mod user { Ok(_) => {} Err(e) => { dbg!("{:?}", e); - return format!("Failure to minecraft username {:?}", username); + 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); + return format!("No UID found for {username:?}"); } Some(x) => { match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await { @@ -185,14 +190,17 @@ pub(crate) mod server { use super::*; pub(crate) mod add { - use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption}; - 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 + // this is to manage the server side of commands related to minecraft use super::*; - use skynet_discord_bot::common::minecraft::update_server; - use skynet_discord_bot::common::minecraft::Minecraft; - use skynet_discord_bot::Config; + use skynet_discord_bot::{ + common::minecraft::{update_server, Minecraft}, + Config, + }; pub fn register() -> CreateCommand { CreateCommand::new("minecraft_add") @@ -220,16 +228,15 @@ pub(crate) mod server { return String::from("Expected Server ID"); }; - let db_lock = { + let db = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() }; - let db = db_lock.read().await; 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); } } @@ -260,12 +267,14 @@ pub(crate) mod server { } pub(crate) mod list { - use serenity::all::CommandInteraction; - use serenity::builder::CreateCommand; - use serenity::client::Context; - use skynet_discord_bot::common::database::DataBase; - use skynet_discord_bot::common::minecraft::{get_minecraft_config_server, server_information}; - use skynet_discord_bot::Config; + 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() -> CreateCommand { CreateCommand::new("minecraft_list") @@ -279,11 +288,10 @@ pub(crate) mod server { Some(x) => x, }; - let db_lock = { + let db = { let data_read = ctx.data.read().await; data_read.get::().expect("Expected Databse in TypeMap.").clone() }; - let db = db_lock.read().await; let servers = get_minecraft_config_server(&db, g_id).await; @@ -320,12 +328,13 @@ pub(crate) mod server { } pub(crate) mod delete { - use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption}; - use serenity::builder::CreateCommand; - use serenity::client::Context; - use serenity::model::id::GuildId; - use skynet_discord_bot::common::database::DataBase; - use skynet_discord_bot::common::minecraft::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() -> CreateCommand { @@ -354,16 +363,15 @@ pub(crate) mod server { return String::from("Expected Server ID"); }; - let db_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected Databse in TypeMap.").clone() + let db = { + let data = ctx.data.read().await; + data.get::().expect("Expected Databse in TypeMap.").clone() }; - let db = db_lock.read().await; 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); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b513fc2..5815541 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,4 +3,5 @@ pub mod committee; 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 index f58803b..0573b37 100644 --- a/src/commands/role_adder.rs +++ b/src/commands/role_adder.rs @@ -62,11 +62,10 @@ pub mod edit { false }; - let db_lock = { + let db = { 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 { @@ -79,8 +78,8 @@ pub mod edit { match add_server(&db, &server_data, delete).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:?}"); } } @@ -101,9 +100,9 @@ pub mod edit { } if delete { - format!("Removed {} + {} = {}", role_a_name, role_b_name, role_c_name) + format!("Removed {role_a_name} + {role_b_name} = {role_c_name}") } else { - format!("Added {} + {} = {}", role_a_name, role_b_name, role_c_name) + format!("Added {role_a_name} + {role_b_name} = {role_c_name}") } } @@ -142,13 +141,12 @@ pub mod edit { pub mod list {} pub mod tools { - use serenity::client::Context; - use serenity::model::guild::Member; + 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 + // check if the role changed is part of the ones for this server if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>( r#" SELECT * @@ -164,7 +162,7 @@ pub mod tools { let mut roles_remove = vec![]; for role_adder in role_adders { - // if the user has both A dnd B give them C + // if the user has both A and 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); @@ -180,13 +178,13 @@ pub mod tools { if !roles_add.is_empty() { if let Err(e) = new_data.add_roles(&ctx, &roles_add).await { - println!("{:?}", e); + println!("{e:?}"); } } if !roles_remove.is_empty() { if let Err(e) = new_data.remove_roles(&ctx, &roles_remove).await { - println!("{:?}", e); + println!("{e:?}"); } } } diff --git a/src/commands/server_icon.rs b/src/commands/server_icon.rs new file mode 100644 index 0000000..d4a78d5 --- /dev/null +++ b/src/commands/server_icon.rs @@ -0,0 +1,223 @@ +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 = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + + 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 = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + + 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 = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected Databse in TypeMap.").clone() + }; + + 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 fence + 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 index 92c6ec0..843ca70 100644 --- a/src/commands/wolves.rs +++ b/src/commands/wolves.rs @@ -4,11 +4,16 @@ use lettre::{ Message, SmtpTransport, Transport, }; use maud::html; -use serenity::all::CommandOptionType; -use serenity::builder::CreateCommandOption; -use serenity::{builder::CreateCommand, client::Context, model::id::UserId}; -use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify}; -use skynet_discord_bot::{get_now_iso, random_string, Config}; +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 { @@ -16,11 +21,10 @@ pub mod link { use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { - let db_lock = { + let db = { 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; @@ -96,7 +100,7 @@ pub mod link { return "Email already verified".to_string(); } - // generate a auth key + // generate an 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 { @@ -110,7 +114,7 @@ pub mod link { } } - 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) + 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 { @@ -205,7 +209,7 @@ pub mod link { .subject("Skynet: Link Discord to Wolves.") .multipart( // This is composed of two parts. - // also helps not trip spam settings (uneven number of url's + // also helps not trip spam settings (uneven number of urls) 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())), @@ -279,22 +283,40 @@ pub mod link { } } +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, GuildId, RoleId}; - use serenity::model::user::User; - use skynet_discord_bot::common::database::get_server_config; - use skynet_discord_bot::common::database::{ServerMembersWolves, Servers}; - use skynet_discord_bot::common::wolves::committees::Committees; + 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 db = { 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 { @@ -341,12 +363,12 @@ pub mod verify { "Discord username linked to Wolves".to_string() } Err(e) => { - println!("{:?}", e); + println!("{e:?}"); "Failed to save, please try /link_wolves again".to_string() } }; } - Err(e) => println!("{:?}", e), + Err(e) => println!("{e:?}"), } "Failed to verify".to_string() @@ -403,7 +425,7 @@ pub mod verify { } if let Err(e) = member.add_roles(&ctx, &roles).await { - println!("{:?}", e); + println!("{e:?}"); } } } @@ -419,7 +441,7 @@ pub mod verify { WHERE committee LIKE ?1 "#, ) - .bind(format!("%{}%", wolves_id)) + .bind(format!("%{wolves_id}%")) .fetch_all(db) .await .unwrap_or_else(|e| { @@ -429,12 +451,18 @@ pub mod verify { } 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 + // if they are a member of one or more committees, and in teh committee server then give them the 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 = GuildId::new(1220150752656363520); - let committee_member = RoleId::new(1226602779968274573); + 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(); @@ -464,13 +492,12 @@ pub mod unlink { use sqlx::{Pool, Sqlite}; pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { - let db_lock = { + let db = { 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 + // doesn't matter if there is one or not, it will be removed regardless delete_link(&db, &command.user.id).await; "Discord link removed".to_string() @@ -516,4 +543,5 @@ pub fn register() -> CreateCommand { .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 index 0663799..2eaf5df 100644 --- a/src/common/database.rs +++ b/src/common/database.rs @@ -1,17 +1,21 @@ use crate::Config; use serde::{Deserialize, Serialize}; -use serenity::model::guild; -use serenity::model::id::{ChannelId, GuildId, RoleId, UserId}; -use serenity::prelude::TypeMapKey; -use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow}; -use sqlx::{Error, FromRow, Pool, Row, Sqlite}; -use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::RwLock; +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}; pub struct DataBase; impl TypeMapKey for DataBase { - type Value = Arc>>; + type Value = Arc>; } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -220,7 +224,7 @@ pub async fn db_init(config: &Config) -> Result, Error> { let pool = SqlitePoolOptions::new() .max_connections(5) .connect_with( - SqliteConnectOptions::from_str(&format!("sqlite://{}", database))? + SqliteConnectOptions::from_str(&format!("sqlite://{database}"))? .foreign_keys(true) .create_if_missing(true), ) diff --git a/src/common/minecraft.rs b/src/common/minecraft.rs index 4faa8ed..77cd754 100644 --- a/src/common/minecraft.rs +++ b/src/common/minecraft.rs @@ -1,10 +1,7 @@ -use crate::common::set_roles::normal::get_server_member_bulk; -use crate::Config; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +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; -use sqlx::{Error, FromRow, Pool, Row, Sqlite}; +use sqlx::{sqlite::SqliteRow, Error, FromRow, Pool, Row, Sqlite}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Minecraft { @@ -27,7 +24,7 @@ impl<'r> FromRow<'r, SqliteRow> for Minecraft { /** loop through all members of server get a list of folks with mc accounts that are members -and a list that arent members +and a list that aren't members */ pub async fn update_server(server_id: &str, db: &Pool, g_id: &GuildId, config: &Config) { let mut usernames = vec![]; @@ -112,7 +109,7 @@ pub async fn whitelist_wipe(server: &str, token: &str) { }; 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 + // recreate the 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 @@ -155,7 +152,7 @@ pub async fn get_minecraft_config_server(db: &Pool, g_id: GuildId) -> Ve } pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) { - println!("Update whitelist for {}", server); + println!("Update whitelist for {server}"); let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}"); let bearer = format!("Bearer {token}"); diff --git a/src/common/mod.rs b/src/common/mod.rs index 38f457a..9e1745e 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -2,3 +2,6 @@ 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..f2c50cc --- /dev/null +++ b/src/common/renderer.rs @@ -0,0 +1,193 @@ +// 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 yoinked it from here. + +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +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 separated 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..1a7c2ef --- /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 hasn't 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 index 0dc7cc5..e11c637 100644 --- a/src/common/set_roles.rs +++ b/src/common/set_roles.rs @@ -1,18 +1,27 @@ pub mod normal { - use crate::common::database::{DataBase, ServerMembersWolves, Servers, Wolves}; - use crate::get_now_iso; - use serenity::client::Context; - use serenity::model::id::{GuildId, RoleId, UserId}; + 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 db = { 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, @@ -20,7 +29,12 @@ pub mod normal { .. } = server; - let mut roles_set = [0, 0, 0]; + 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 { @@ -32,28 +46,30 @@ pub mod normal { 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 + // members_changed acts as an override to only deal with the 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[0] += 1; + roles_set.new += 1; roles.push(role.to_owned()); } } if !member.roles.contains(role_current) { - roles_set[1] += 1; + roles_set.current_add += 1; roles.push(role_current.to_owned()); } if let Err(e) = member.add_roles(ctx, &roles).await { - println!("{:?}", e); + println!("{e:?}"); } } else { // old and never @@ -65,16 +81,16 @@ pub mod normal { } if member.roles.contains(role_current) { - roles_set[2] += 1; - // if theya re not a current member and have the role then remove it + roles_set.current_rem += 1; + // if they'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); + println!("{e:?}"); } } } for role in remove_roles.iter().flatten() { if let Err(e) = member.remove_role(ctx, role).await { - println!("{:?}", e); + println!("{e:?}"); } } } @@ -83,7 +99,14 @@ pub mod normal { 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.get(), roles_set[0], roles_set[1], roles_set[2]); + 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 { @@ -123,7 +146,7 @@ pub mod normal { Ok(_) => {} Err(e) => { println!("Failure to insert into {}", server.get()); - println!("{:?}", e); + println!("{e:?}"); } } } @@ -131,58 +154,56 @@ pub mod normal { // for updating committee members pub mod committee { - use crate::common::database::{get_channel_from_row, get_role_from_row, DataBase, Wolves}; - use crate::common::wolves::committees::Committees; - use crate::Config; + 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, GuildId}; - use serenity::builder::CreateChannel; - use serenity::client::Context; - use serenity::model::channel::ChannelType; - use serenity::model::guild::Member; - use serenity::model::id::ChannelId; - use serenity::model::prelude::RoleId; - use sqlx::sqlite::SqliteRow; - use sqlx::{Error, FromRow, Pool, Row, Sqlite}; + 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; - use std::sync::Arc; - pub async fn check_committee(ctx: Arc) { - let db_lock = { + pub async fn check_committee(ctx: &Context) { + let db = { 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 = GuildId::new(1220150752656363520); + 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; + 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 + This function can take a Vec of members (or just one) and gives them the appropriate roles on teh committee server */ - pub async fn update_committees(db: &Pool, ctx: &Context, _config: &Config, members: &mut Vec) { - let server = GuildId::new(1220150752656363520); - let committee_member = RoleId::new(1226602779968274573); - let committees = get_committees(db).await; - let categories = [ - ChannelId::new(1226606560973815839), - // C&S Chats 2 - ChannelId::new(1341457244973305927), - // C&S Chats 3 - ChannelId::new(1341457509717639279), - ]; + 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(); @@ -203,11 +224,11 @@ pub mod committee { let mut channels = server.channels(&ctx).await.unwrap_or_default(); - // a map of users and the roles they are goign to be getting + // a map of users and the roles they are going 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 + // we need to create roles and channels if they don't already exist let mut category_index = 0; let mut i = 0; loop { @@ -308,21 +329,21 @@ pub mod committee { // 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) => { - let mut tmp = x.to_owned(); - if !tmp.is_empty() { - tmp.push(committee_member); - } - tmp - } + 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 @@ -331,14 +352,25 @@ pub mod committee { 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()); } } - if !roles_required.is_empty() { - // if there are committee roles then give the general purporse role - roles_add.push(committee_member); + 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 purpose 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; } @@ -357,8 +389,6 @@ pub mod committee { 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(); - } else { - member.remove_roles(&ctx, &[committee_member]).await.unwrap_or_default(); } } @@ -406,10 +436,10 @@ pub mod committee { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CommitteeRoles { id_wolves: i64, - id_role: RoleId, - id_channel: ChannelId, + pub id_role: RoleId, + pub id_channel: ChannelId, pub name_role: String, - name_channel: String, + pub name_channel: String, pub count: i64, } @@ -446,8 +476,8 @@ pub mod committee { { Ok(_) => {} Err(e) => { - println!("Failure to insert into Wolves {:?}", role); - println!("{:?}", e); + println!("Failure to insert into Wolves {role:?}"); + println!("{e:?}"); } } } @@ -464,13 +494,13 @@ pub mod committee { .await .unwrap_or_else(|e| { println!("Failure to get Roles from committee_roles"); - println!("{:?}", e); + println!("{e:?}"); vec![] }) } - pub async fn get_committees(db: &Pool) -> Vec { - sqlx::query_as::<_, Committees>( + pub async fn get_committees(db: &Pool) -> Option> { + match sqlx::query_as::<_, Committees>( r#" SELECT * FROM committees @@ -478,10 +508,13 @@ pub mod committee { ) .fetch_all(db) .await - .unwrap_or_else(|e| { - dbg!(e); - vec![] - }) + { + Ok(x) => Some(x), + Err(e) => { + dbg!(e); + None + } + } } async fn get_server_member_discord(db: &Pool, user: &i64) -> Option { diff --git a/src/common/wolves.rs b/src/common/wolves.rs index 6f73842..3744558 100644 --- a/src/common/wolves.rs +++ b/src/common/wolves.rs @@ -38,8 +38,8 @@ async fn add_users_wolves(db: &Pool, user: &WolvesResultUserMin) { { Ok(_) => {} Err(e) => { - println!("Failure to insert into Wolves {:?}", user); - println!("{:?}", e); + println!("Failure to insert into Wolves {user:?}"); + println!("{e:?}"); } } } @@ -48,12 +48,14 @@ async fn add_users_wolves(db: &Pool, user: &WolvesResultUserMin) { This is getting data for Clubs and Socs */ pub mod cns { - use crate::common::database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers}; - use crate::common::set_roles::normal::update_server; - use crate::common::wolves::{add_users_wolves, WolvesResultUserMin}; - use crate::Config; - use serenity::client::Context; - use serenity::model::id::GuildId; + 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; @@ -67,11 +69,10 @@ pub mod cns { } pub async fn get_wolves(ctx: &Context) { - let db_lock = { + let db = { 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; @@ -96,7 +97,6 @@ pub mod cns { 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![]; let mut server_name_tmp = None; for user in wolves.get_members(wolves_api).await { // dbg!(&user.committee); @@ -115,10 +115,6 @@ pub mod cns { add_users_wolves(&db, &WolvesResultUserMin::from(&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); - } } } } @@ -129,9 +125,6 @@ pub mod cns { set_server_member(&db, server, cs_id).await; } } - if !user_to_update.is_empty() { - update_server(ctx, &server_config, &[], &user_to_update).await; - } } } @@ -151,7 +144,7 @@ pub mod cns { Ok(_) => {} Err(e) => { println!("Failure to set server name {}", server.get()); - println!("{:?}", e); + println!("{e:?}"); } } } @@ -190,7 +183,7 @@ pub mod cns { Ok(_) => {} Err(e) => { println!("Failure to insert into ServerMembers {} {:?}", server.get(), user); - println!("{:?}", e); + println!("{e:?}"); } } } @@ -200,8 +193,7 @@ pub mod cns { Get and store the data on C&S committees */ pub mod committees { - use crate::common::database::DataBase; - use crate::Config; + use crate::{common::database::DataBase, Config}; use serenity::client::Context; use sqlx::{Pool, Sqlite}; @@ -231,11 +223,10 @@ pub mod committees { } pub async fn get_cns(ctx: &Context) { - let db_lock = { + let db = { 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; @@ -270,8 +261,8 @@ pub mod committees { { Ok(_) => {} Err(e) => { - println!("Failure to insert into Committees {:?}", committee); - println!("{:?}", e); + println!("Failure to insert into Committees {committee:?}"); + println!("{e:?}"); } } } diff --git a/src/lib.rs b/src/lib.rs index 762dc69..1a6afb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,10 @@ pub mod common; use chrono::{Datelike, SecondsFormat, Utc}; use dotenvy::dotenv; use rand::{distr::Alphanumeric, rng, Rng}; -use serenity::model::id::{ChannelId, GuildId, RoleId}; -use serenity::prelude::TypeMapKey; +use serenity::{ + model::id::{ChannelId, GuildId, RoleId}, + prelude::TypeMapKey, +}; use std::{env, sync::Arc}; use tokio::sync::RwLock; @@ -32,7 +34,10 @@ pub struct Config { // discord server for committee pub committee_server: GuildId, pub committee_role: RoleId, - pub committee_category: ChannelId, + pub committee_category: Vec, + + // items pertaining to CompSoc only + pub compsoc_server: GuildId, } impl TypeMapKey for Config { type Value = Arc>; @@ -57,7 +62,8 @@ pub fn get_config() -> Config { wolves_api: "".to_string(), committee_server: GuildId::new(1), committee_role: RoleId::new(1), - committee_category: ChannelId::new(1), + committee_category: vec![], + compsoc_server: GuildId::new(1), }; if let Ok(x) = env::var("DATABASE_HOME") { @@ -99,14 +105,22 @@ pub fn get_config() -> Config { config.committee_server = GuildId::new(x); } } - if let Ok(x) = env::var("COMMITTEE_DISCORD") { + 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.committee_category = ChannelId::new(x); + config.compsoc_server = GuildId::new(x); } } diff --git a/src/main.rs b/src/main.rs index 6d5e849..8dff675 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,28 @@ pub mod commands; use crate::commands::role_adder::tools::on_role_change; -use serenity::all::{ActivityData, Command, CreateMessage, EditInteractionResponse, GuildId, GuildMemberUpdateEvent, Interaction}; -use serenity::model::guild::Member; use serenity::{ + all::{Command, CommandDataOptionValue, CreateMessage, EditInteractionResponse, Interaction}, async_trait, client::{Context, EventHandler}, + gateway::{ActivityData, ChunkGuildFilter}, model::{ + event::GuildMemberUpdateEvent, gateway::{GatewayIntents, Ready}, + guild::Member, + id::GuildId, user::OnlineStatus, }, Client, }; -use skynet_discord_bot::common::database::{db_init, get_server_config, get_server_member, DataBase}; -use skynet_discord_bot::common::set_roles::committee::update_committees; -use skynet_discord_bot::common::wolves::committees::Committees; -use skynet_discord_bot::{get_config, Config}; +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 tokio::sync::RwLock; @@ -24,15 +31,21 @@ struct Handler; #[async_trait] impl EventHandler for Handler { + // 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 db = { 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() @@ -40,7 +53,7 @@ impl EventHandler for Handler { let config_global = config_lock.read().await; // committee server takes priority - let committee_server = GuildId::new(1220150752656363520); + 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; @@ -66,7 +79,7 @@ impl EventHandler for Handler { } if let Err(e) = new_member.add_roles(&ctx, &roles).await { - println!("{:?}", e); + println!("{e:?}"); } } else { let tmp = get_committee(&db, config_server.wolves_id).await; @@ -94,14 +107,12 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use // 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 db = { 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 + // check if the role changed is part of the ones for this server if let Some(x) = new_data { on_role_change(&db, &ctx, x).await; } @@ -111,6 +122,12 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use println!("[Main] {} is connected!", ready.user.name); ctx.set_presence(Some(ActivityData::playing("with humanity's fate")), OnlineStatus::Online); + 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![ @@ -125,27 +142,34 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use { Ok(_) => {} Err(e) => { - println!("{:?}", e) + println!("{e:?}") } } - match GuildId::new(1220150752656363520) - .set_commands(&ctx.http, vec![commands::count::committee::register()]) + // 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) - } - } - - match GuildId::new(689189992417067052) - .set_commands(&ctx.http, vec![commands::count::servers::register()]) - .await - { - Ok(_) => {} - Err(e) => { - println!("{:?}", e) + println!("{e:?}") } } } @@ -153,7 +177,7 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use async fn interaction_create(&self, ctx: Context, interaction: Interaction) { if let Interaction::Command(command) = interaction { let _ = command.defer_ephemeral(&ctx.http).await; - //println!("Received command interaction: {:#?}", command); + // println!("Received command interaction: {:#?}", command); let content = match command.data.name.as_str() { // user commands @@ -164,6 +188,7 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use "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()), }, @@ -175,6 +200,19 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use 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()), }, @@ -191,11 +229,34 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use &_ => 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_response(&ctx.http, EditInteractionResponse::new().content(content)).await { - println!("Cannot respond to slash command: {}", why); + println!("Cannot respond to slash command: {why}"); } } } @@ -230,7 +291,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"); @@ -238,7 +300,7 @@ async fn main() { let mut data = client.data.write().await; data.insert::(Arc::new(RwLock::new(config))); - data.insert::(Arc::new(RwLock::new(db))); + data.insert::(Arc::new(db)); } // Finally, start a single shard, and start listening to events. @@ -246,6 +308,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:?}"); } }