Compare commits

..

No commits in common. "main" and "#17-automate-onboarding-mk-ii" have entirely different histories.

42 changed files with 603 additions and 3068 deletions

View file

@ -1,10 +0,0 @@
on:
- push
- workflow_dispatch
jobs:
check_lfs:
# nix/docker
runs-on: nix
steps:
- uses: https://github.com/MPLew-is/lfs-check-action@1

View file

@ -1,51 +0,0 @@
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

View file

@ -1,2 +0,0 @@
# Fix typos
7e90f451965b0edbd331765ad295a02f31d2bf24

View file

@ -7,4 +7,3 @@ fn_params_layout = "Compressed"
struct_lit_width = 0 struct_lit_width = 0
tab_spaces = 2 tab_spaces = 2
use_small_heuristics = "Max" use_small_heuristics = "Max"
imports_granularity = "Crate"

View file

@ -1,6 +0,0 @@
# this file controls the
[source]
repo = "https://forgejo.skynet.ie/Computer_Society/open-goverance"
directory = "Resources/Logo_Variants"
file = "_festivals.toml"

View file

@ -1,2 +0,0 @@
[formatting]
column_width = 120

557
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -110,12 +110,6 @@ version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@ -389,12 +383,6 @@ version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.5.0" version = "1.5.0"
@ -461,39 +449,6 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -682,12 +637,6 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "data-url"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -841,16 +790,6 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -866,15 +805,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.33" version = "1.0.33"
@ -885,12 +815,6 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.9.2" version = "0.9.2"
@ -925,27 +849,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" 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]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@ -1160,16 +1063,6 @@ dependencies = [
"polyval", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.0" version = "0.31.0"
@ -1693,6 +1586,16 @@ dependencies = [
"syn 2.0.89", "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]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@ -1714,18 +1617,6 @@ dependencies = [
"icu_properties", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.5.0" version = "2.5.0"
@ -1786,12 +1677,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.70" version = "0.3.70"
@ -1801,24 +1686,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "kv-log-macro" name = "kv-log-macro"
version = "1.0.7" version = "1.0.7"
@ -1851,7 +1718,7 @@ dependencies = [
"futures-util", "futures-util",
"hostname", "hostname",
"httpdate", "httpdate",
"idna", "idna 1.0.3",
"mime", "mime",
"native-tls", "native-tls",
"nom", "nom",
@ -1976,15 +1843,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -2014,7 +1872,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32",
] ]
[[package]] [[package]]
@ -2174,12 +2031,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "owo-colors"
version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -2224,12 +2075,6 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.7" version = "1.1.7"
@ -2300,19 +2145,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 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]] [[package]]
name = "polling" name = "polling"
version = "3.7.4" version = "3.7.4"
@ -2507,12 +2339,6 @@ dependencies = [
"rand_core 0.5.1", "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]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.4" version = "0.5.4"
@ -2609,34 +2435,6 @@ dependencies = [
"windows-registry", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.8" version = "0.17.8"
@ -2652,34 +2450,6 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.6" version = "0.9.6"
@ -2812,22 +2582,6 @@ dependencies = [
"untrusted", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -2959,15 +2713,6 @@ dependencies = [
"thiserror 1.0.63", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -3061,15 +2806,6 @@ dependencies = [
"digest 0.10.7", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -3095,49 +2831,21 @@ dependencies = [
"rand_core 0.6.4", "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]] [[package]]
name = "skynet_discord_bot" name = "skynet_discord_bot"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"color-eyre",
"dotenvy", "dotenvy",
"eyre",
"lettre", "lettre",
"maud", "maud",
"rand 0.9.0", "rand 0.9.0",
"resvg",
"serde", "serde",
"serde_json", "serde_json",
"serenity", "serenity",
"sqlx", "sqlx",
"surf", "surf",
"tiny-skia",
"tokio", "tokio",
"toml",
"usvg",
"usvg-text-layout",
"wolves_oxidised", "wolves_oxidised",
] ]
@ -3472,15 +3180,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" 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]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.5" version = "0.1.5"
@ -3521,36 +3220,6 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -3694,16 +3363,6 @@ dependencies = [
"syn 2.0.89", "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]] [[package]]
name = "time" name = "time"
version = "0.2.27" version = "0.2.27"
@ -3773,31 +3432,6 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.7.6" version = "0.7.6"
@ -3947,47 +3581,6 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -4024,17 +3617,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@ -4047,29 +3629,12 @@ dependencies = [
"tracing", "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]] [[package]]
name = "try-lock" name = "try-lock"
version = "0.2.5" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ttf-parser"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0609f771ad9c6155384897e1df4d948e692667cc0588548b68eb44d052b27633"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"
@ -4118,24 +3683,6 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
@ -4157,18 +3704,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" 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]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.4.0" version = "0.4.0"
@ -4187,49 +3722,16 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna 0.5.0",
"percent-encoding", "percent-encoding",
"serde", "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]] [[package]]
name = "utf-8" name = "utf-8"
version = "0.7.6" version = "0.7.6"
@ -4248,12 +3750,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.10.0" version = "1.10.0"
@ -4419,12 +3915,6 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "weezl"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.5.2" version = "1.5.2"
@ -4654,15 +4144,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.50.0" version = "0.50.0"
@ -4685,7 +4166,7 @@ dependencies = [
[[package]] [[package]]
name = "wolves_oxidised" name = "wolves_oxidised"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#265c8c81d1eb870a6149da5ce72556d44f57937f" source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#74f00e3dcfd52744583b0ded08efb8bb27fdcec8"
dependencies = [ dependencies = [
"reqwest 0.12.9", "reqwest 0.12.9",
"serde", "serde",
@ -4705,12 +4186,6 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.7.5" version = "0.7.5"

View file

@ -4,21 +4,16 @@ version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]] [[bin]]
name = "update_data" name = "update_data"
[[bin]] [[bin]]
name = "update_committee" name = "update_users"
[[bin]] [[bin]]
name = "update_minecraft" name = "update_minecraft"
[[bin]]
name = "update_server-icon"
[[bin]]
name = "cleanup_committee"
[dependencies] [dependencies]
# discord library # discord library
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
@ -34,7 +29,7 @@ surf = "2.3"
dotenvy = "0.15" dotenvy = "0.15"
# For sqlite # 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"] } serde_json = { version = "1.0", features = ["raw_value"] }
# create random strings # create random strings
@ -47,13 +42,4 @@ chrono = "0.4"
lettre = "0.11" lettre = "0.11"
maud = "0.27" maud = "0.27"
toml = "0.8.23"
serde = "1.0" 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"

View file

@ -1,7 +1,7 @@
# Skynet Discord Bot # Skynet Discord Bot
The Skynet bot is designed to manage users on Discord. 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. It allows users to link their UL Wolves account with Wolves in a GDPR compliant manner.
Skynet (bot) is hosted by the Computer Society on Skynet (computer cluster). Skynet (bot) is hosted is hosted by the Computer Society on Skynet (computer cluster).
## Documentation ## Documentation
We have split up the documentation into different segments depending on who the user is. We have split up the documentation into different segments depending on who the user is.

View file

@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS committee_roles (
id_wolves integer PRIMARY KEY,
id_role integer DEFAULT 1,
id_channel integer DEFAULT 1,
-- not strictly required but for readability and debugging
name_role text NOT NULL DEFAULT '',
name_channel text NOT NULL DEFAULT '',
count integer DEFAULT 0
);

View file

@ -1,9 +0,0 @@
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);

View file

@ -1,6 +0,0 @@
-- No need for this col since it is goign to be in "committees" anyways
ALTER TABLE servers DROP COLUMN server_name;
-- we do care about teh ID of the club/soc though
ALTER TABLE servers ADD COLUMN wolves_id integer DEFAULT 0;

View file

@ -7,7 +7,7 @@ For example is a user links on the CompSoc Discord then they will also get their
## Setup - Committee ## Setup - Committee
You need admin access to run any of the commands in this section. You need admin access to run any of the commands in this section.
Either the server owner or a user with the ``Manage Server`` permission. Either the server owner or a user with the ``Administrator`` permission.
### Get the API Key ### Get the API Key
The ``api_key`` is used by the Bot in order to request information, it will be used later in the process. The ``api_key`` is used by the Bot in order to request information, it will be used later in the process.
@ -38,9 +38,9 @@ The reason for both roles is ye have one for active members while the second is
### Setup Bot ### Setup Bot
This is where the bot is configured. This is where the bot is configured.
You will need the ``api_key`` from the start of the process. You will need the ``api_key`` from the start of the process.
You (personally) will need a role with ``Manage Server`` permission to be able to do this. You (personally) will need a role with ``Administrator`` permission to be able to do this.
1. Use the command ``/committee add`` and a list of options will pop up. 1. Use the command ``/add`` and a list of options will pop up.
2. ``api_key`` is the key you got from Keith earlier. 2. ``api_key`` is the key you got from Keith earlier.
3. ``role_current`` is the ``member-current`` that you created earlier. 3. ``role_current`` is the ``member-current`` that you created earlier.
4. ``role_past`` (optional) is the role for all current and past members. 4. ``role_past`` (optional) is the role for all current and past members.

View file

@ -8,13 +8,13 @@ This is to link your Discord account with your UL Wolves account.
**You will only need to do this once**. **You will only need to do this once**.
### Setup ### Setup
1. In a Discord server with the Skynet Bot enter ``/wolves link YOUR_WOLVES_CONTACT_EMAIL`` 1. In a Discord server with the Skynet Bot enter ``/link_wolves YOUR_WOLVES_CONTACT_EMAIL``
<img src="../media/setup_user_01.png" alt="link process start" width="50%" height="50%"> <img src="../media/setup_user_01.png" alt="link process start" width="50%" height="50%">
* Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email in the Contact Email here: <https://ulwolves.ie/memberships/profile> * Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email in the Contact Email here: <https://ulwolves.ie/memberships/profile>
* This is most likely your student mail * This is most likely your student mail
2. An email will be sent to you with a verification code. 2. An email will be sent to you with a verification code.
<img src="../media/setup_user_02.png" alt="signup email" width="50%" height="50%"> <img src="../media/setup_user_02.png" alt="signup email" width="50%" height="50%">
3. Verify the code using ``/wolves verify CODE_FROM_EMAIL`` in Discord. 3. Verify the code using ``/verify CODE_FROM_EMAIL`` in Discord.
<img src="../media/setup_user_03.png" alt="verify in discord" width="50%" height="50%"> <img src="../media/setup_user_03.png" alt="verify in discord" width="50%" height="50%">
4. Once complete your Wolves and Discord accounts will be linked. 4. Once complete your Wolves and Discord accounts will be linked.
@ -23,4 +23,4 @@ You will get member roles on any Discord that is using the bot that you are a me
### Minecraft ### Minecraft
You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society. You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society.
``/wolves link_minecraft MINECRAFT_USERNAME`` ``/link_minecraft MINECRAFT_USERNAME``

17
flake.lock generated
View file

@ -32,22 +32,6 @@
"type": "indirect" "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": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1722995383, "lastModified": 1722995383,
@ -67,7 +51,6 @@
"inputs": { "inputs": {
"naersk": "naersk", "naersk": "naersk",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs_2",
"nixpkgs-mozilla": "nixpkgs-mozilla",
"utils": "utils" "utils": "utils"
} }
}, },

View file

@ -4,10 +4,6 @@
inputs = { inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable"; nixpkgs.url = "nixpkgs/nixos-unstable";
naersk.url = "github:nix-community/naersk"; naersk.url = "github:nix-community/naersk";
nixpkgs-mozilla = {
url = "github:mozilla/nixpkgs-mozilla";
flake = false;
};
utils.url = "github:numtide/flake-utils"; utils.url = "github:numtide/flake-utils";
}; };
@ -21,33 +17,15 @@
nixpkgs, nixpkgs,
utils, utils,
naersk, naersk,
nixpkgs-mozilla,
}: }:
utils.lib.eachDefaultSystem ( utils.lib.eachDefaultSystem (
system: let system: let
overrides = (builtins.fromTOML (builtins.readFile ./rust-toolchain.toml)); pkgs = (import nixpkgs) {inherit system;};
pkgs = (import nixpkgs) { naersk' = pkgs.callPackage naersk {};
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"; package_name = "skynet_discord_bot";
desc = "Skynet Discord Bot"; desc = "Skynet Discord Bot";
buildInputs = with pkgs; [ buildInputs = with pkgs; [
openssl openssl
glib
gdk-pixbuf
pkg-config pkg-config
rustfmt rustfmt
]; ];
@ -58,10 +36,6 @@
pname = "${package_name}"; pname = "${package_name}";
src = ./.; src = ./.;
buildInputs = buildInputs; buildInputs = buildInputs;
postInstall = ''
mkdir $out/config
cp .server-icons.toml $out/config
'';
}; };
# Run `nix build .#fmt` to run tests # Run `nix build .#fmt` to run tests
fmt = naersk'.buildPackage { fmt = naersk'.buildPackage {
@ -88,15 +62,7 @@
# `nix develop` # `nix develop`
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [rustup rustPlatform.bindgenHook]; nativeBuildInputs = with pkgs; [rustc cargo pkg-config openssl rustfmt];
# libraries here
buildInputs = buildInputs;
RUSTC_VERSION = overrides.toolchain.channel;
# https://github.com/rust-lang/rust-bindgen#environment-variables
shellHook = ''
export PATH="''${CARGO_HOME:-~/.cargo}/bin":"$PATH"
export PATH="''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-${pkgs.stdenv.hostPlatform.rust.rustcTarget}/bin":"$PATH"
'';
}; };
nixosModule = { nixosModule = {
@ -122,14 +88,12 @@
wantedBy = []; wantedBy = [];
after = ["network-online.target"]; after = ["network-online.target"];
environment = environment_config; environment = environment_config;
path = with pkgs; [ git git-lfs ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
User = "${cfg.user}"; User = "${cfg.user}";
Group = "${cfg.user}"; Group = "${cfg.user}";
ExecStart = "${self.defaultPackage."${system}"}/bin/${script}"; ExecStart = "${self.defaultPackage."${system}"}/bin/${script}";
# kill each service if its ran for 9 min
TimeoutStartSec=540;
EnvironmentFile = [ EnvironmentFile = [
"${cfg.env.discord}" "${cfg.env.discord}"
"${cfg.env.mail}" "${cfg.env.mail}"
@ -154,17 +118,12 @@
# modify these # modify these
scripts = { scripts = {
# every 10 min # every 20 min
"update_data" = "*:0,10,20,30,40,50"; "update_data" = "*:0,10,20,30,40,50";
# groups are updated every hour, offset from teh ldap # groups are updated every hour, offset from teh ldap
"update_users" = "*:5,15,25,35,45,55"; "update_users" = "*:05:00";
# Committee server has its own timer
"update_committee" = "*:5,15,25,35,45,55";
# minecraft stuff is updated at 5am # minecraft stuff is updated at 5am
# this service does not depend on teh discord cache
"update_minecraft" = "5:10:00"; "update_minecraft" = "5:10:00";
# server icon gets updated daily at midnight
"update_server-icon" = "0:01:00";
}; };
in { in {
options.services."${package_name}" = { options.services."${package_name}" = {
@ -224,7 +183,6 @@
after = ["network-online.target"]; after = ["network-online.target"];
wants = []; wants = [];
environment = environment_config; environment = environment_config;
path = with pkgs; [ git git-lfs ];
serviceConfig = { serviceConfig = {
User = "${cfg.user}"; User = "${cfg.user}";

BIN
media/setup_user_01.png (Stored with Git LFS)

Binary file not shown.

BIN
media/setup_user_03.png (Stored with Git LFS)

Binary file not shown.

View file

@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "1.87" channel = "1.80"

View file

@ -1,137 +0,0 @@
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::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(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<GuildId>) {
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().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::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().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<Sqlite>, 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::<Vec<_>>();
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::<Vec<_>>();
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);
}
}
}
}
}

View file

@ -1,74 +0,0 @@
use serenity::{
all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent},
async_trait,
client::{Context, EventHandler},
model::gateway::GatewayIntents,
Client,
};
use skynet_discord_bot::{
common::{
database::{db_init, DataBase},
set_roles::committee,
},
get_config, Config,
};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
#[tokio::main]
async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(_) => return,
};
// Intents are a bitflag, bitwise operations can be used to dictate which intents to use
let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS;
// Build our client.
let mut client = Client::builder(&config.discord_token, intents)
.event_handler(Handler {})
.cache_settings(serenity::cache::Settings::default())
.await
.expect("Error creating client");
{
let mut data = client.data.write().await;
data.insert::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(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<GuildId>) {
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config_global = config_lock.read().await;
let server = config_global.committee_server;
ctx.shard.chunk_guild(server, Some(2000), false, ChunkGuildFilter::None, None);
println!("Cache loaded");
}
async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent) {
if (chunk.chunk_index + 1) == chunk.chunk_count {
println!("Cache built successfully!");
// u[date committee server
committee::check_committee(&ctx).await;
// finish up
process::exit(0);
}
}
}

View file

@ -4,13 +4,10 @@ use serenity::{
model::gateway::{GatewayIntents, Ready}, model::gateway::{GatewayIntents, Ready},
Client, Client,
}; };
use skynet_discord_bot::{ use skynet_discord_bot::common::database::{db_init, DataBase};
common::{ use skynet_discord_bot::common::wolves::cns::get_wolves;
database::{db_init, DataBase}, use skynet_discord_bot::common::wolves::committees::get_cns;
wolves::{cns::get_wolves, committees::get_cns}, use skynet_discord_bot::{get_config, Config};
},
get_config, Config,
};
use std::{process, sync::Arc}; use std::{process, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -30,7 +27,6 @@ async fn main() {
// Build our client. // Build our client.
let mut client = Client::builder(&config.discord_token, intents) let mut client = Client::builder(&config.discord_token, intents)
.event_handler(Handler {}) .event_handler(Handler {})
.cache_settings(serenity::cache::Settings::default())
.await .await
.expect("Error creating client"); .expect("Error creating client");
@ -38,11 +34,11 @@ async fn main() {
let mut data = client.data.write().await; let mut data = client.data.write().await;
data.insert::<Config>(Arc::new(RwLock::new(config))); data.insert::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(Arc::new(db)); data.insert::<DataBase>(Arc::new(RwLock::new(db)));
} }
if let Err(why) = client.start().await { if let Err(why) = client.start().await {
println!("Client error: {why:?}"); println!("Client error: {:?}", why);
} }
} }

View file

@ -1,10 +1,6 @@
use skynet_discord_bot::{ use skynet_discord_bot::common::database::db_init;
common::{ use skynet_discord_bot::common::minecraft::{get_minecraft_config, update_server, whitelist_wipe};
database::db_init, use skynet_discord_bot::get_config;
minecraft::{get_minecraft_config, update_server, whitelist_wipe},
},
get_config,
};
use std::collections::HashSet; use std::collections::HashSet;
#[tokio::main] #[tokio::main]

View file

@ -1,71 +0,0 @@
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::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(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::<DataBase>().expect("Expected Config in TypeMap.").clone()
};
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().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);
}
}

View file

@ -1,24 +1,13 @@
use serenity::{ use serenity::{
all::{ChunkGuildFilter, GuildId, GuildMembersChunkEvent},
async_trait, async_trait,
client::{Context, EventHandler}, client::{Context, EventHandler},
model::gateway::GatewayIntents, model::gateway::{GatewayIntents, Ready},
Client, Client,
}; };
use skynet_discord_bot::{ use skynet_discord_bot::common::database::{db_init, get_server_config_bulk, DataBase};
common::{ use skynet_discord_bot::common::set_roles::{committee, normal};
database::{db_init, get_server_config_bulk, DataBase}, use skynet_discord_bot::{get_config, Config};
set_roles::normal, use std::{process, sync::Arc};
},
get_config, Config,
};
use std::{
process,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[tokio::main] #[tokio::main]
@ -33,11 +22,7 @@ async fn main() {
let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS;
// Build our client. // Build our client.
let mut client = Client::builder(&config.discord_token, intents) 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 .await
.expect("Error creating client"); .expect("Error creating client");
@ -45,51 +30,41 @@ async fn main() {
let mut data = client.data.write().await; let mut data = client.data.write().await;
data.insert::<Config>(Arc::new(RwLock::new(config))); data.insert::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(Arc::new(db)); data.insert::<DataBase>(Arc::new(RwLock::new(db)));
} }
if let Err(why) = client.start().await { 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] #[async_trait]
impl EventHandler for Handler { impl EventHandler for Handler {
async fn cache_ready(&self, ctx: Context, guilds: Vec<GuildId>) { async fn ready(&self, ctx: Context, ready: Ready) {
self.server_count.swap(guilds.len(), Ordering::SeqCst); let ctx = Arc::new(ctx);
for guild in guilds { println!("{} is connected!", ready.user.name);
ctx.shard.chunk_guild(guild, Some(2000), false, ChunkGuildFilter::None, None);
}
println!("Cache loaded {}", &self.server_count.load(Ordering::SeqCst));
}
async fn guild_members_chunk(&self, ctx: Context, chunk: GuildMembersChunkEvent) { // this goes into each server and sets roles for each wolves member
if (chunk.chunk_index + 1) == chunk.chunk_count { check_bulk(Arc::clone(&ctx)).await;
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!");
// this goes into each server and sets roles for each wolves member // u[date committee server
check_bulk(&ctx).await; committee::check_committee(Arc::clone(&ctx)).await;
// finish up // finish up
process::exit(0); process::exit(0);
}
}
} }
} }
async fn check_bulk(ctx: &Context) { async fn check_bulk(ctx: Arc<Context>) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
}; };
let db = db_lock.read().await;
for server_config in get_server_config_bulk(&db).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;
} }
} }

View file

@ -1,88 +1,94 @@
use serenity::{ use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, use serenity::{builder::CreateCommand, client::Context};
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::{ use skynet_discord_bot::common::wolves::cns::get_wolves;
database::{get_server_config, DataBase, Servers}, use skynet_discord_bot::is_admin;
set_roles::normal::update_server,
wolves::cns::get_wolves,
};
use sqlx::{Error, Pool, Sqlite}; use sqlx::{Error, Pool, Sqlite};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let sub_options = if let Some(CommandDataOption { // check if user has high enough permisssions
value: CommandDataOptionValue::SubCommand(options), if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let wolves_api = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(key),
.. ..
}) = command.data.options.first() }) = command.data.options.first()
{ {
options key.to_string()
} else {
return "Please provide sub options".to_string();
};
let wolves_api = if let Some(x) = sub_options.first() {
match &x.value {
CommandDataOptionValue::String(key) => key.to_string(),
_ => return "Please provide a wolves API key".to_string(),
}
} else { } else {
return "Please provide a wolves API key".to_string(); return "Please provide a wolves API key".to_string();
}; };
let role_current = if let Some(x) = sub_options.get(1) { let role_current = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Role(role),
CommandDataOptionValue::Role(role) => role.to_owned(), ..
_ => return "Please provide a valid role for ``Role Current``".to_string(), }) = command.data.options.get(1)
} {
role.to_owned()
} else { } else {
return "Please provide a valid role for ``Role Current``".to_string(); return "Please provide a valid role for ``Role Current``".to_string();
}; };
let role_past = if let Some(x) = sub_options.get(5) { let role_past = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Role(role),
CommandDataOptionValue::Role(role) => Some(role.to_owned()), ..
_ => None, }) = command.data.options.get(5)
} {
Some(role.to_owned())
} else { } else {
None None
}; };
let bot_channel_id = if let Some(x) = sub_options.get(2) { let bot_channel_id = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Channel(channel),
CommandDataOptionValue::Channel(channel) => channel.to_owned(), ..
_ => return "Please provide a valid channel for ``Bot Channel``".to_string(), }) = command.data.options.get(2)
} {
channel.to_owned()
} else { } else {
return "Please provide a valid channel for ``Bot Channel``".to_string(); return "Please provide a valid channel for ``Bot Channel``".to_string();
}; };
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let server_data = Servers { let server_data = Servers {
server: command.guild_id.unwrap_or_default(), server: command.guild_id.unwrap_or_default(),
wolves_api, wolves_api,
wolves_id: 0,
role_past, role_past,
role_current, role_current,
member_past: 0, member_past: 0,
member_current: 0, member_current: 0,
bot_channel_id, bot_channel_id,
server_name: "".to_string(),
}; };
match add_server(&db, ctx, &server_data).await { match add_server(&db, ctx, &server_data).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("{e:?}"); println!("{:?}", e);
return format!("Failure to insert into Servers {server_data:?}"); return format!("Failure to insert into Servers {:?}", server_data);
} }
} }
"Added/Updated server info".to_string() "Added/Updated server info".to_string()
} }
pub fn register() -> CreateCommand {
CreateCommand::new("add")
.description("Enable the bot for this discord")
.add_option(CreateCommandOption::new(CommandOptionType::String, "api_key", "UL Wolves API Key").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_current", "Role for Current members").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Channel, "bot_channel", "Safe space for folks to use the bot commands.").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_past", "Role for Past members").required(false))
}
async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Result<Option<Servers>, Error> { async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Result<Option<Servers>, Error> {
let existing = get_server_config(db, &server.server).await; let existing = get_server_config(db, &server.server).await;
let role_past = server.role_past.map(|x| x.get() as i64); let role_past = server.role_past.map(|x| x.get() as i64);
@ -101,7 +107,7 @@ async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Resul
.fetch_optional(db) .fetch_optional(db)
.await; .await;
// if the entry does not exist already then do a user update // if the entry does not exist already tehn do a user update
let (update, current_remove, current_role, past_remove, past_role) = match &existing { let (update, current_remove, current_role, past_remove, past_role) = match &existing {
None => (true, false, None, false, None), None => (true, false, None, false, None),
Some(x) => { Some(x) => {

View file

@ -1,27 +0,0 @@
use serenity::all::{CommandOptionType, CreateCommand, CreateCommandOption};
pub fn register() -> CreateCommand {
CreateCommand::new("committee")
.description("Commands related to what committees can do")
.default_member_permissions(serenity::model::Permissions::MANAGE_GUILD)
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "add", "Enable the bot for this discord")
.add_sub_option(CreateCommandOption::new(CommandOptionType::String, "api_key", "UL Wolves API Key").required(true))
.add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_current", "Role for Current members").required(true))
.add_sub_option(
CreateCommandOption::new(CommandOptionType::Channel, "bot_channel", "Safe space for folks to use the bot commands.").required(true),
)
.add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_past", "Role for Past members").required(false)),
)
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "roles_adder", "Combine roles together to an new one")
.add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_a", "A role you want to add to Role B").required(true))
.add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_b", "A role you want to add to Role A").required(true))
.add_sub_option(CreateCommandOption::new(CommandOptionType::Role, "role_c", "Sum of A and B").required(true))
.add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "delete", "Delete this entry.").required(false)),
)
.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.")),
)
}

View file

@ -1,259 +0,0 @@
pub mod committee {
// get the list of all the current clubs/socs members
use serenity::all::{
CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption,
};
use skynet_discord_bot::common::{database::DataBase, set_roles::committee::db_roles_get};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let sub_options = if let Some(CommandDataOption {
value: CommandDataOptionValue::SubCommand(key),
..
}) = command.data.options.first()
{
key
} else {
return "Please provide a wolves API key".to_string();
};
let all = if let Some(x) = sub_options.first() {
match x.value {
CommandDataOptionValue::Boolean(y) => y,
_ => false,
}
} else {
false
};
let db = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let mut cs = vec![];
// pull it from a DB
for committee in db_roles_get(&db).await {
if !all && committee.count == 0 {
continue;
}
cs.push((committee.count, committee.name_role.to_owned()));
}
cs.sort_by_key(|(count, _)| *count);
cs.reverse();
// msg can be a max 2000 chars long
let mut limit = 2000 - 3;
let mut response = vec!["```".to_string()];
for (count, name) in cs {
let leading = if count < 10 { " " } else { "" };
let line = format!("{leading}{count} {name}");
let length = line.len() + 1;
if length < (limit + 3) {
response.push(line);
limit -= length;
} else {
break;
}
}
response.push("```".to_string());
response.join("\n")
}
pub fn register() -> CreateCommand {
CreateCommand::new("count")
.description("Count Committee Members")
// All Committee members are able to add reactions to posts
.default_member_permissions(serenity::model::Permissions::ADD_REACTIONS)
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "committee", "List out the Committee Roles Numbers")
.add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "all", "List out all the Committee Roles Numbers").required(false)),
)
}
}
pub mod servers {
// get the list of all the current clubs/socs
use serde::{Deserialize, Serialize};
use serenity::all::{CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption};
use skynet_discord_bot::{
common::{
database::{get_server_config_bulk, DataBase},
set_roles::committee::get_committees,
},
get_now_iso,
};
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
pub async fn run(_command: &CommandInteraction, ctx: &Context) -> String {
let db = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let mut committees = HashMap::new();
if let Some(x) = get_committees(&db).await {
for committee in x {
committees.insert(committee.id, committee.to_owned());
}
}
let mut cs = vec![];
// pull it from a DB
for server_config in get_server_config_bulk(&db).await {
if let Some(x) = committees.get(&server_config.wolves_id) {
cs.push((server_config.member_current, server_config.member_past, x.name_full.to_owned()));
}
}
// get all members
let (wolves_current, wolves_past, total) = get_wolves_total(&db).await;
cs.push((total, total, String::from("Skynet Network")));
cs.push((wolves_current, wolves_past, String::from("Clubs/Socs Servers")));
// treat teh committee server as its own thing
let committee_current = get_wolves_committee(&db).await;
cs.push((committee_current, committee_current, String::from("Committee Server")));
cs.sort_by_key(|(current, _, _)| *current);
cs.reverse();
// msg can be a max 2000 chars long
let mut limit = 2000 - 3;
let mut response = vec!["```".to_string()];
for (current, past, name) in cs {
let current_leading = if current < 10 {
" "
} else if current < 100 {
" "
} else {
""
};
let past_leading = if past < 10 {
" "
} else if past < 100 {
" "
} else {
""
};
let line = format!("{current_leading}{current} {past_leading}{past} {name}");
let length = line.len() + 1;
// +3 is to account for the closing fence
if length < (limit + 3) {
response.push(line);
limit -= length;
} else {
break;
}
}
response.push("```".to_string());
response.join("\n")
}
#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
pub struct Count {
pub count: i64,
}
async fn get_wolves_total(db: &Pool<Sqlite>) -> (i64, i64, i64) {
let current = match sqlx::query_as::<_, Count>(
r#"
SELECT COUNT(DISTINCT id_wolves) as count
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
wolves.discord IS NOT NULL
AND server_members.expiry > ?
)
"#,
)
.bind(get_now_iso(true))
.fetch_one(db)
.await
{
Ok(res) => res.count,
Err(e) => {
dbg!(e);
0
}
};
let cns = match sqlx::query_as::<_, Count>(
r#"
SELECT COUNT(DISTINCT id_wolves) as count
FROM server_members
JOIN wolves USING (id_wolves)
WHERE wolves.discord IS NOT NULL
"#,
)
.bind(get_now_iso(true))
.fetch_one(db)
.await
{
Ok(res) => res.count,
Err(e) => {
dbg!(e);
0
}
};
let total = match sqlx::query_as::<_, Count>(
r#"
SELECT COUNT(DISTINCT id_wolves) as count
FROM wolves
WHERE discord IS NOT NULL
"#,
)
.fetch_one(db)
.await
{
Ok(res) => res.count,
Err(e) => {
dbg!(e);
0
}
};
(current, cns, total)
}
async fn get_wolves_committee(db: &Pool<Sqlite>) -> i64 {
// expiry
match sqlx::query_as::<_, Count>(
"
SELECT count
FROM committee_roles
WHERE id_wolves = '0'
",
)
.fetch_one(db)
.await
{
Ok(res) => res.count,
Err(e) => {
dbg!(e);
0
}
}
}
pub fn register() -> CreateCommand {
CreateCommand::new("count")
.description("Count the servers")
.default_member_permissions(serenity::model::Permissions::MANAGE_GUILD)
.add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "servers", "List out all servers using the skynet bot"))
}
}

View file

@ -4,27 +4,22 @@ use lettre::{
Message, SmtpTransport, Transport, Message, SmtpTransport, Transport,
}; };
use maud::html; use maud::html;
use serenity::{ use serenity::{builder::CreateCommand, client::Context, model::id::UserId};
all::CommandOptionType, use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify};
builder::{CreateCommand, CreateCommandOption}, use skynet_discord_bot::{get_now_iso, random_string, Config};
client::Context,
model::id::UserId,
};
use skynet_discord_bot::{
common::database::{DataBase, Wolves, WolvesVerify},
get_now_iso, random_string, Config,
};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
pub mod link { pub mod link {
use super::*; use super::*;
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; use serde::{Deserialize, Serialize};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
@ -42,23 +37,14 @@ pub mod link {
return "Linking already in process, please check email.".to_string(); return "Linking already in process, please check email.".to_string();
} }
let sub_options = if let Some(CommandDataOption { let email = if let Some(CommandDataOption {
value: CommandDataOptionValue::SubCommand(options), value: CommandDataOptionValue::String(email),
.. ..
}) = command.data.options.first() }) = command.data.options.first()
{ {
options email.trim()
} else { } else {
return "Please provide sub options".to_string(); return "Please provide a valid user".to_string();
};
let email = if let Some(x) = sub_options.first() {
match &x.value {
CommandDataOptionValue::String(email) => email.trim(),
_ => return "Please provide a valid email".to_string(),
}
} else {
return "Please provide a valid email".to_string();
}; };
// check if email exists // check if email exists
@ -100,9 +86,9 @@ pub mod link {
return "Email already verified".to_string(); return "Email already verified".to_string();
} }
// generate an auth key // generate a auth key
let auth = random_string(20); let auth = random_string(20);
match send_mail(&config, &details.email, &auth, &command.user.name) { match send_mail(&config, &details, &auth, &command.user.name) {
Ok(_) => match save_to_db(&db, &details, &auth, &command.user.id).await { Ok(_) => match save_to_db(&db, &details, &auth, &command.user.id).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@ -114,7 +100,13 @@ pub mod link {
} }
} }
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.") format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email)
}
pub fn register() -> CreateCommand {
CreateCommand::new("link_wolves")
.description("Set Wolves Email")
.add_option(CreateCommandOption::new(CommandOptionType::String, "email", "UL Wolves Email").required(true))
} }
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> { pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
@ -145,35 +137,33 @@ pub mod link {
.ok() .ok()
} }
fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result<smtp::response::Response, smtp::Error> { fn send_mail(config: &Config, email: &Wolves, auth: &str, user: &str) -> Result<smtp::response::Response, smtp::Error> {
let discord = "https://computer.discord.skynet.ie"; let mail = &email.email;
let discord = "https://discord.skynet.ie";
let sender = format!("UL Computer Society <{}>", &config.mail_user); let sender = format!("UL Computer Society <{}>", &config.mail_user);
// Create the html we want to send. // Create the html we want to send.
let html = html! { let html = html! {
head { head {
title { "UL Wolves Discord Linker" } title { "Hello from Skynet!" }
style type="text/css" { style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }" "h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
} }
} }
div { div {
h2 { "UL Wolves Discord Linker" } h2 { "Hello from Skynet!" }
h3 { "Link your UL Wolves Account to Discord" }
// Substitute in the name of our recipient. // Substitute in the name of our recipient.
p { "Hi " (user) "," } p { "Hi " (user) "," }
p { p {
"Please paste this line into Discord (and press enter) to verify your discord account:" "Please use " pre { "/verify code: " (auth)} " to verify your discord account."
br;
pre { "/wolves verify code: " (auth)}
} }
hr;
h3 { "Help & Support" }
p { p {
"If you have issues please refer to our Computer Society Discord Server:" "If you have issues please refer to our Discord server:"
br; br;
a href=(discord) { (discord) } a href=(discord) { (discord) }
}
p {
"Skynet Team"
br; br;
"UL Computer Society" "UL Computer Society"
} }
@ -182,23 +172,15 @@ pub mod link {
let body_text = format!( let body_text = format!(
r#" r#"
UL Wolves Discord Linker Hi {user}
Link your UL Wolves Account to Discord
Link your Account Please use "/verify code: {auth}" to verify your discord account.
Hi {user}, If you have issues please refer to our Discord server:
{discord}
Please paste this line into Discord (and press enter) to verify your Discord account: Skynet Team
/wolves verify code: {auth} UL Computer Society
-------------------------------------------------------------------------
Help & Support
If you have issues please refer to our Computer Society Discord Server:
{discord}
UL Computer Society
"# "#
); );
@ -206,10 +188,10 @@ pub mod link {
let email = Message::builder() let email = Message::builder()
.from(sender.parse().unwrap()) .from(sender.parse().unwrap())
.to(mail.parse().unwrap()) .to(mail.parse().unwrap())
.subject("Skynet: Link Discord to Wolves.") .subject("Skynet-Discord: Link Wolves.")
.multipart( .multipart(
// This is composed of two parts. // This is composed of two parts.
// also helps not trip spam settings (uneven number of urls) // also helps not trip spam settings (uneven number of url's
MultiPart::alternative() MultiPart::alternative()
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text)) .singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text))
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())), .singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())),
@ -268,6 +250,19 @@ pub mod link {
.await .await
} }
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum WolvesResultUserResult {
B(bool),
S(String),
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
success: i64,
result: WolvesResultUserResult,
}
async fn save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> { async fn save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> {
sqlx::query_as::<_, Wolves>( sqlx::query_as::<_, Wolves>(
" "
@ -283,70 +278,42 @@ 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 { pub mod verify {
use super::*; use super::*;
use crate::commands::wolves::link::{db_pending_clear_expired, get_server_member_discord, get_verify_from_db}; use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db};
use serenity::{ use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, use serenity::model::user::User;
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::{
database::{get_server_config, ServerMembersWolves, Servers},
wolves::committees::Committees,
};
use sqlx::Error; use sqlx::Error;
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
// check if user has used /link_wolves // check if user has used /link_wolves
let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await { let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await {
x x
} else { } else {
return "Please use ''/wolves link'' first".to_string(); return "Please use /link_wolves first".to_string();
}; };
let sub_options = if let Some(CommandDataOption { let code = if let Some(CommandDataOption {
value: CommandDataOptionValue::SubCommand(options), value: CommandDataOptionValue::String(code),
.. ..
}) = command.data.options.first() }) = command.data.options.first()
{ {
options code
} else {
return "Please provide sub options".to_string();
};
let code = if let Some(x) = sub_options.first() {
match &x.value {
CommandDataOptionValue::String(y) => y.trim(),
_ => return "Please provide a verification code".to_string(),
}
} else { } else {
return "Please provide a verification code".to_string(); return "Please provide a verification code".to_string();
}; };
db_pending_clear_expired(&db).await; db_pending_clear_expired(&db).await;
if details.auth_code != code { if &details.auth_code != code {
return "Invalid verification code".to_string(); return "Invalid verification code".to_string();
} }
@ -356,24 +323,26 @@ pub mod verify {
Ok(_) => { Ok(_) => {
// get teh right roles for the user // get teh right roles for the user
set_server_roles(&db, &command.user, ctx).await; set_server_roles(&db, &command.user, ctx).await;
// check if they are a committee member, and on that server
set_server_roles_committee(&db, &command.user, ctx).await;
"Discord username linked to Wolves".to_string() "Discord username linked to Wolves".to_string()
} }
Err(e) => { Err(e) => {
println!("{e:?}"); println!("{:?}", e);
"Failed to save, please try /link_wolves again".to_string() "Failed to save, please try /link_wolves again".to_string()
} }
}; };
} }
Err(e) => println!("{e:?}"), Err(e) => println!("{:?}", e),
} }
"Failed to verify".to_string() "Failed to verify".to_string()
} }
pub fn register() -> CreateCommand {
CreateCommand::new("verify")
.description("Verify Wolves Email")
.add_option(CreateCommandOption::new(CommandOptionType::String, "code", "Code from verification email").required(true))
}
async fn db_pending_clear_successful(pool: &Pool<Sqlite>, user: &UserId) -> Result<Option<WolvesVerify>, Error> { async fn db_pending_clear_successful(pool: &Pool<Sqlite>, user: &UserId) -> Result<Option<WolvesVerify>, Error> {
sqlx::query_as::<_, WolvesVerify>( sqlx::query_as::<_, WolvesVerify>(
r#" r#"
@ -425,7 +394,7 @@ pub mod verify {
} }
if let Err(e) = member.add_roles(&ctx, &roles).await { if let Err(e) = member.add_roles(&ctx, &roles).await {
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -433,44 +402,6 @@ pub mod verify {
} }
} }
async fn get_committees_id(db: &Pool<Sqlite>, wolves_id: i64) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
FROM committees
WHERE committee LIKE ?1
"#,
)
.bind(format!("%{wolves_id}%"))
.fetch_all(db)
.await
.unwrap_or_else(|e| {
dbg!(e);
vec![]
})
}
async fn set_server_roles_committee(db: &Pool<Sqlite>, discord: &User, ctx: &Context) {
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().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 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 = config.committee_server;
let committee_member = config.committee_role;
if let Ok(member) = server.member(ctx, &discord.id).await {
member.add_roles(&ctx, &[committee_member]).await.unwrap_or_default();
}
}
}
}
async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<ServerMembersWolves>, Error> { async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<ServerMembersWolves>, Error> {
sqlx::query_as::<_, ServerMembersWolves>( sqlx::query_as::<_, ServerMembersWolves>(
" "
@ -485,63 +416,3 @@ pub mod verify {
.await .await
} }
} }
pub mod unlink {
use serenity::all::{CommandInteraction, Context, UserId};
use skynet_discord_bot::common::database::{DataBase, Wolves};
use sqlx::{Pool, Sqlite};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
// 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()
}
async fn delete_link(db: &Pool<Sqlite>, user: &UserId) {
match sqlx::query_as::<_, Wolves>(
"
UPDATE wolves
SET discord = NULL
WHERE discord = ?1;
",
)
.bind(user.get() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
dbg!(e);
}
}
}
}
pub fn register() -> CreateCommand {
CreateCommand::new("wolves")
.description("Commands related to UL Wolves")
// link
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "link", "Link your Wolves account to your Discord")
.add_sub_option(CreateCommandOption::new(CommandOptionType::String, "email", "UL Wolves Email").required(true)),
)
// verify
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "verify", "Verify Wolves Email")
.add_sub_option(CreateCommandOption::new(CommandOptionType::String, "code", "Code from verification email").required(true)),
)
// unlink
.add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "unlink", "Unlink your Wolves account from your Discord"))
.add_option(
CreateCommandOption::new(CommandOptionType::SubCommand, "link_minecraft", "Link your minecraft account")
.add_sub_option(CreateCommandOption::new(CommandOptionType::String, "minecraft_username", "Your Minecraft username").required(true))
.add_sub_option(CreateCommandOption::new(CommandOptionType::Boolean, "bedrock_account", "Is this a Bedrock account?").required(false)),
)
.add_option(CreateCommandOption::new(CommandOptionType::SubCommand, "docs", "Link to where the documentation can be found."))
}

View file

@ -1,4 +1,4 @@
use serenity::client::Context; use serenity::{builder::CreateCommand, client::Context};
use skynet_discord_bot::common::database::DataBase; use skynet_discord_bot::common::database::DataBase;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
@ -7,26 +7,28 @@ pub(crate) mod user {
use super::*; use super::*;
pub(crate) mod add { pub(crate) mod add {
use super::*; use super::*;
use crate::commands::wolves::link::get_server_member_discord; use crate::commands::link_email::link::get_server_member_discord;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serenity::{ use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}, use serenity::model::id::UserId;
model::id::UserId, use skynet_discord_bot::common::database::Wolves;
}; use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft};
use skynet_discord_bot::{ use skynet_discord_bot::Config;
common::{
database::Wolves,
minecraft::{whitelist_update, Minecraft},
},
Config,
};
use sqlx::Error; use sqlx::Error;
pub fn register() -> CreateCommand {
CreateCommand::new("link_minecraft")
.description("Link your minecraft account")
.add_option(CreateCommandOption::new(CommandOptionType::String, "minecraft_username", "Your Minecraft username").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "bedrock_account", "Is this a Bedrock account?").required(false))
}
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
@ -36,33 +38,26 @@ pub(crate) mod user {
// user has to have previously linked with wolves // user has to have previously linked with wolves
if get_server_member_discord(&db, &command.user.id).await.is_none() { if get_server_member_discord(&db, &command.user.id).await.is_none() {
return "Not linked with wolves, please use ``/wolves link`` with your wolves email.".to_string(); return "Not linked with wolves, please use ``/link_wolves`` with your wolves email.".to_string();
} }
let sub_options = if let Some(CommandDataOption { let username = if let Some(CommandDataOption {
value: CommandDataOptionValue::SubCommand(options), value: CommandDataOptionValue::String(username),
.. ..
}) = command.data.options.first() }) = command.data.options.first()
{ {
options username.trim()
} else {
return "Please provide sub options".to_string();
};
let username = if let Some(x) = sub_options.first() {
match &x.value {
CommandDataOptionValue::String(username) => username.trim(),
_ => return "Please provide a valid username".to_string(),
}
} else { } else {
return "Please provide a valid username".to_string(); return "Please provide a valid username".to_string();
}; };
let java = if let Some(x) = sub_options.get(1) { // this is always true unless they state its not
match &x.value { let java = if let Some(CommandDataOption {
CommandDataOptionValue::Boolean(z) => !z, value: CommandDataOptionValue::Boolean(z),
_ => true, ..
} }) = command.data.options.get(1)
{
!z
} else { } else {
true true
}; };
@ -74,14 +69,14 @@ pub(crate) mod user {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
dbg!("{:?}", e); dbg!("{:?}", e);
return format!("Failure to minecraft username {username:?}"); return format!("Failure to minecraft username {:?}", username);
} }
} }
username_mc = username.to_string(); username_mc = username.to_string();
} else { } else {
match get_minecraft_bedrock(username, &config.minecraft_mcprofile).await { match get_minecraft_bedrock(username, &config.minecraft_mcprofile).await {
None => { None => {
return format!("No UID found for {username:?}"); return format!("No UID found for {:?}", username);
} }
Some(x) => { Some(x) => {
match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await { match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await {
@ -190,29 +185,26 @@ pub(crate) mod server {
use super::*; use super::*;
pub(crate) mod add { pub(crate) mod add {
use serenity::{ use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption}, use serenity::model::id::GuildId;
model::id::GuildId,
};
use sqlx::Error; use sqlx::Error;
// this is to manage the server side of commands related to minecraft // this is to managfe the server side of commands related to minecraft
use super::*; use super::*;
use skynet_discord_bot::{ use skynet_discord_bot::common::minecraft::update_server;
common::minecraft::{update_server, Minecraft}, use skynet_discord_bot::common::minecraft::Minecraft;
Config, use skynet_discord_bot::{is_admin, Config};
};
pub fn register() -> CreateCommand { pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_add") CreateCommand::new("minecraft_add").description("Add a minecraft server").add_option(
.description("Add a minecraft server") CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
.default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) )
.add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society")
.required(true),
)
} }
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id { let g_id = match command.guild_id {
None => return "Not in a server".to_string(), None => return "Not in a server".to_string(),
Some(x) => x, Some(x) => x,
@ -228,15 +220,16 @@ pub(crate) mod server {
return String::from("Expected Server ID"); return String::from("Expected Server ID");
}; };
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
match add_server(&db, &g_id, &server_minecraft).await { match add_server(&db, &g_id, &server_minecraft).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("{e:?}"); println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft); return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
} }
} }
@ -267,31 +260,31 @@ pub(crate) mod server {
} }
pub(crate) mod list { pub(crate) mod list {
use serenity::{all::CommandInteraction, builder::CreateCommand, client::Context}; use serenity::all::CommandInteraction;
use skynet_discord_bot::{ use serenity::builder::CreateCommand;
common::{ use serenity::client::Context;
database::DataBase, use skynet_discord_bot::common::database::DataBase;
minecraft::{get_minecraft_config_server, server_information}, use skynet_discord_bot::common::minecraft::{get_minecraft_config_server, server_information};
}, use skynet_discord_bot::{is_admin, Config};
Config,
};
pub fn register() -> CreateCommand { pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_list") CreateCommand::new("minecraft_list").description("List your minecraft servers")
.default_member_permissions(serenity::model::Permissions::MANAGE_GUILD)
.description("List your minecraft servers")
} }
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id { let g_id = match command.guild_id {
None => return "Not in a server".to_string(), None => return "Not in a server".to_string(),
Some(x) => x, Some(x) => x,
}; };
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let servers = get_minecraft_config_server(&db, g_id).await; let servers = get_minecraft_config_server(&db, g_id).await;
@ -328,26 +321,26 @@ pub(crate) mod server {
} }
pub(crate) mod delete { pub(crate) mod delete {
use serenity::{ use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption}, use serenity::builder::CreateCommand;
builder::CreateCommand, use serenity::client::Context;
client::Context, use serenity::model::id::GuildId;
model::id::GuildId, use skynet_discord_bot::common::database::DataBase;
}; use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::common::{database::DataBase, minecraft::Minecraft}; use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite}; use sqlx::{Error, Pool, Sqlite};
pub fn register() -> CreateCommand { pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_delete") CreateCommand::new("minecraft_delete").description("Delete a minecraft server").add_option(
.description("Delete a minecraft server") CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
.default_member_permissions(serenity::model::Permissions::MANAGE_GUILD) )
.add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society")
.required(true),
)
} }
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id { let g_id = match command.guild_id {
None => return "Not in a server".to_string(), None => return "Not in a server".to_string(),
Some(x) => x, Some(x) => x,
@ -363,15 +356,16 @@ pub(crate) mod server {
return String::from("Expected Server ID"); return String::from("Expected Server ID");
}; };
let db = { let db_lock = {
let data = ctx.data.read().await; let data_read = ctx.data.read().await;
data.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
match server_remove(&db, &g_id, &server_minecraft).await { match server_remove(&db, &g_id, &server_minecraft).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("{e:?}"); println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft); return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
} }
} }

View file

@ -1,7 +1,4 @@
pub mod add_server; pub mod add_server;
pub mod committee; pub mod link_email;
pub mod count;
pub mod minecraft; pub mod minecraft;
pub mod role_adder; pub mod role_adder;
pub mod server_icon;
pub mod wolves;

View file

@ -1,48 +1,47 @@
use serenity::client::Context; use serenity::client::Context;
use skynet_discord_bot::common::database::{DataBase, RoleAdder}; use skynet_discord_bot::common::database::{DataBase, RoleAdder};
use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite}; use sqlx::{Error, Pool, Sqlite};
pub mod edit { pub mod edit {
use super::*; use super::*;
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction}; use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String { pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let sub_options = if let Some(CommandDataOption { // check if user has high enough permisssions
value: CommandDataOptionValue::SubCommand(options), if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let role_a = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
.. ..
}) = command.data.options.first() }) = command.data.options.first()
{ {
options role.to_owned()
} else { } else {
return "Please provide sub options".to_string(); return "Please provide a valid role for ``Role Current``".to_string();
}; };
let role_a = if let Some(x) = sub_options.first() { let role_b = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Role(role),
CommandDataOptionValue::Role(role) => role.to_owned(), ..
_ => return "Please provide a valid role for ``Role A``".to_string(), }) = command.data.options.get(1)
} {
role.to_owned()
} else { } else {
return "Please provide a valid role for ``Role A``".to_string(); return "Please provide a valid role for ``Role Current``".to_string();
}; };
let role_b = if let Some(x) = sub_options.get(1) { let role_c = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Role(role),
CommandDataOptionValue::Role(role) => role.to_owned(), ..
_ => return "Please provide a valid role for ``Role B``".to_string(), }) = command.data.options.get(2)
} {
role.to_owned()
} else { } else {
return "Please provide a valid role for ``Role B``".to_string(); return "Please provide a valid role for ``Role Current``".to_string();
};
let role_c = if let Some(x) = sub_options.get(2) {
match &x.value {
CommandDataOptionValue::Role(role) => role.to_owned(),
_ => return "Please provide a valid role for ``Role C``".to_string(),
}
} else {
return "Please provide a valid role for ``Role C``".to_string();
}; };
if role_a == role_b { if role_a == role_b {
@ -53,19 +52,21 @@ pub mod edit {
return "Role C cannot be same as A or B".to_string(); return "Role C cannot be same as A or B".to_string();
} }
let delete = if let Some(x) = sub_options.get(3) { let delete = if let Some(CommandDataOption {
match &x.value { value: CommandDataOptionValue::Boolean(z),
CommandDataOptionValue::Boolean(z) => *z, ..
_ => false, }) = command.data.options.get(3)
} {
*z
} else { } else {
false false
}; };
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let server = command.guild_id.unwrap_or_default(); let server = command.guild_id.unwrap_or_default();
let server_data = RoleAdder { let server_data = RoleAdder {
@ -78,8 +79,8 @@ pub mod edit {
match add_server(&db, &server_data, delete).await { match add_server(&db, &server_data, delete).await {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("{e:?}"); println!("{:?}", e);
return format!("Failure to insert into Servers {server_data:?}"); return format!("Failure to insert into Servers {:?}", server_data);
} }
} }
@ -100,12 +101,21 @@ pub mod edit {
} }
if delete { 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 { } else {
format!("Added {role_a_name} + {role_b_name} = {role_c_name}") format!("Added {} + {} = {}", role_a_name, role_b_name, role_c_name)
} }
} }
pub fn register() -> CreateCommand {
CreateCommand::new("roles_adder")
.description("Combine roles together to an new one")
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_a", "A role you want to add to Role B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_b", "A role you want to add to Role A").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_c", "Sum of A and B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "delete", "Delete this entry.").required(false))
}
async fn add_server(db: &Pool<Sqlite>, server: &RoleAdder, delete: bool) -> Result<Option<RoleAdder>, Error> { async fn add_server(db: &Pool<Sqlite>, server: &RoleAdder, delete: bool) -> Result<Option<RoleAdder>, Error> {
if delete { if delete {
sqlx::query_as::<_, RoleAdder>( sqlx::query_as::<_, RoleAdder>(
@ -141,12 +151,13 @@ pub mod edit {
pub mod list {} pub mod list {}
pub mod tools { pub mod tools {
use serenity::{client::Context, model::guild::Member}; use serenity::client::Context;
use serenity::model::guild::Member;
use skynet_discord_bot::common::database::RoleAdder; use skynet_discord_bot::common::database::RoleAdder;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, new_data: Member) { pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, new_data: Member) {
// check if the role changed is part of the ones for this server // check if the role changed is part of the oens for this server
if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>( if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>(
r#" r#"
SELECT * SELECT *
@ -162,7 +173,7 @@ pub mod tools {
let mut roles_remove = vec![]; let mut roles_remove = vec![];
for role_adder in role_adders { for role_adder in role_adders {
// if the user has both A and B give them C // if the user has both A dnd B give them C
if new_data.roles.contains(&role_adder.role_a) && new_data.roles.contains(&role_adder.role_b) && !new_data.roles.contains(&role_adder.role_c) 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); roles_add.push(role_adder.role_c);
@ -178,13 +189,13 @@ pub mod tools {
if !roles_add.is_empty() { if !roles_add.is_empty() {
if let Err(e) = new_data.add_roles(&ctx, &roles_add).await { if let Err(e) = new_data.add_roles(&ctx, &roles_add).await {
println!("{e:?}"); println!("{:?}", e);
} }
} }
if !roles_remove.is_empty() { if !roles_remove.is_empty() {
if let Err(e) = new_data.remove_roles(&ctx, &roles_remove).await { if let Err(e) = new_data.remove_roles(&ctx, &roles_remove).await {
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }

View file

@ -1,223 +0,0 @@
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::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().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::<DataBase>().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<Sqlite>) -> Option<ServerIcons> {
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::<Config>().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::<DataBase>().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<Sqlite>) -> Vec<CountResult> {
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")
}
}
}

View file

@ -1,21 +1,17 @@
use crate::Config; use crate::Config;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serenity::{ use serenity::model::guild;
model::{ use serenity::model::id::{ChannelId, GuildId, RoleId, UserId};
guild, use serenity::prelude::TypeMapKey;
id::{ChannelId, GuildId, RoleId, UserId}, use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow};
}, use sqlx::{Error, FromRow, Pool, Row, Sqlite};
prelude::TypeMapKey, use std::str::FromStr;
}; use std::sync::Arc;
use sqlx::{ use tokio::sync::RwLock;
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},
Error, FromRow, Pool, Row, Sqlite,
};
use std::{str::FromStr, sync::Arc};
pub struct DataBase; pub struct DataBase;
impl TypeMapKey for DataBase { impl TypeMapKey for DataBase {
type Value = Arc<Pool<Sqlite>>; type Value = Arc<RwLock<Pool<Sqlite>>>;
} }
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
@ -125,12 +121,12 @@ impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
pub struct Servers { pub struct Servers {
pub server: GuildId, pub server: GuildId,
pub wolves_api: String, pub wolves_api: String,
pub wolves_id: i64,
pub role_past: Option<RoleId>, pub role_past: Option<RoleId>,
pub role_current: RoleId, pub role_current: RoleId,
pub member_past: i64, pub member_past: i64,
pub member_current: i64, pub member_current: i64,
pub bot_channel_id: ChannelId, pub bot_channel_id: ChannelId,
pub server_name: String,
} }
impl<'r> FromRow<'r, SqliteRow> for Servers { impl<'r> FromRow<'r, SqliteRow> for Servers {
@ -162,12 +158,12 @@ impl<'r> FromRow<'r, SqliteRow> for Servers {
Ok(Self { Ok(Self {
server, server,
wolves_api: row.try_get("wolves_api")?, wolves_api: row.try_get("wolves_api")?,
wolves_id: row.try_get("wolves_id").unwrap_or(0),
role_past, role_past,
role_current, role_current,
member_past: row.try_get("member_past")?, member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?, member_current: row.try_get("member_current")?,
bot_channel_id, bot_channel_id,
server_name: row.try_get("server_name")?,
}) })
} }
} }
@ -194,28 +190,14 @@ impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
} }
} }
pub(crate) fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId { fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
let id = match row.try_get(col) { match row.try_get(col) {
Ok(x) => { Ok(x) => {
let tmp: i64 = x; let tmp: i64 = x;
tmp as u64 RoleId::new(tmp as u64)
} }
_ => 0, _ => RoleId::from(0u64),
}; }
RoleId::from(id)
}
pub(crate) fn get_channel_from_row(row: &SqliteRow, col: &str) -> ChannelId {
let id = match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
tmp as u64
}
_ => 0,
};
ChannelId::from(id)
} }
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> { pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
@ -224,7 +206,7 @@ pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(5) .max_connections(5)
.connect_with( .connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{database}"))? SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true) .foreign_keys(true)
.create_if_missing(true), .create_if_missing(true),
) )

View file

@ -1,7 +1,10 @@
use crate::{common::set_roles::normal::get_server_member_bulk, Config}; use crate::common::set_roles::normal::get_server_member_bulk;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use crate::Config;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serenity::model::id::GuildId; use serenity::model::id::GuildId;
use sqlx::{sqlite::SqliteRow, Error, FromRow, Pool, Row, Sqlite}; use sqlx::sqlite::SqliteRow;
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft { pub struct Minecraft {
@ -24,7 +27,7 @@ impl<'r> FromRow<'r, SqliteRow> for Minecraft {
/** /**
loop through all members of server loop through all members of server
get a list of folks with mc accounts that are members get a list of folks with mc accounts that are members
and a list that aren't members and a list that arent members
*/ */
pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) { pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) {
let mut usernames = vec![]; let mut usernames = vec![];
@ -109,7 +112,7 @@ pub async fn whitelist_wipe(server: &str, token: &str) {
}; };
post(&format!("{url_base}/files/delete"), &bearer, &deletion).await; post(&format!("{url_base}/files/delete"), &bearer, &deletion).await;
// recreate the file, passing in the type here so the compiler knows what type of Vec it is // recreate teh file, passing in the type here so the compiler knows what type of vec it is
post::<Vec<&str>>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await; post::<Vec<&str>>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await;
// reload the whitelist // reload the whitelist
@ -152,7 +155,7 @@ pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Ve
} }
pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) { 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 url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}"); let bearer = format!("Bearer {token}");

View file

@ -2,6 +2,3 @@ pub mod database;
pub mod minecraft; pub mod minecraft;
pub mod set_roles; pub mod set_roles;
pub mod wolves; pub mod wolves;
pub mod renderer;
pub mod server_icon;

View file

@ -1,193 +0,0 @@
// 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<String>),
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<Self> {
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::<Vec<&str>>();
if s.len() < 2 {
dbg!("Invalid color object, try checking help");
return None;
}
Some((s[0].to_string(), s[1].to_string()))
})
.collect::<Vec<Option<(String, String)>>>();
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::<Vec<String>>();
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<String> {
match std::fs::read_to_string(fi) {
Ok(d) => Ok(d),
Err(_) => {
dbg!("File {fi:?} does not exist");
bail!("File {fi:?} does not exist");
}
}
}
}

View file

@ -1,404 +0,0 @@
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<ConfigTomlFestivals>,
}
#[derive(Deserialize)]
pub struct ConfigTomlLocal {
pub source: ConfigTomlSource,
}
#[derive(Deserialize)]
pub struct ConfigTomlRemote {
pub festivals: Vec<ConfigTomlFestivals>,
}
#[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::<ConfigTomlLocal>(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::<ConfigTomlRemote>(&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<Sqlite>, 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<String>,
exclusions: Vec<String>,
}
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<LogoData> {
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<LogoData>) -> Vec<LogoData> {
let mut filtered: Vec<LogoData> = 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<Sqlite>, 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<Sqlite>, 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
}
}

View file

@ -1,27 +1,18 @@
pub mod normal { pub mod normal {
use crate::{ use crate::common::database::{DataBase, ServerMembersWolves, Servers, Wolves};
common::database::{DataBase, ServerMembersWolves, Servers, Wolves}, use crate::get_now_iso;
get_now_iso, use serenity::client::Context;
}; use serenity::model::id::{GuildId, RoleId, UserId};
use serenity::{
client::Context,
model::id::{GuildId, RoleId, UserId},
};
use sqlx::{Pool, Sqlite}; 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<RoleId>], members_changed: &[UserId]) { pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let Servers { let Servers {
server, server,
role_past, role_past,
@ -29,12 +20,7 @@ pub mod normal {
.. ..
} = server; } = server;
let mut roles_set = RolesChange { let mut roles_set = [0, 0, 0];
total: 0,
new: 0,
current_add: 0,
current_rem: 0,
};
let mut members = vec![]; let mut members = vec![];
for member in get_server_member_bulk(&db, server).await { for member in get_server_member_bulk(&db, server).await {
@ -46,30 +32,28 @@ pub mod normal {
if let Ok(x) = server.members(ctx, None, None).await { if let Ok(x) = server.members(ctx, None, None).await {
for member in x { for member in x {
// members_changed acts as an override to only deal with the users in it // members_changed acts as an override to only deal with teh users in it
if !members_changed.is_empty() && !members_changed.contains(&member.user.id) { if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
continue; continue;
} }
if members.contains(&member.user.id) { if members.contains(&member.user.id) {
roles_set.total += 1;
let mut roles = vec![]; let mut roles = vec![];
if let Some(role) = &role_past { if let Some(role) = &role_past {
if !member.roles.contains(role) { if !member.roles.contains(role) {
roles_set.new += 1; roles_set[0] += 1;
roles.push(role.to_owned()); roles.push(role.to_owned());
} }
} }
if !member.roles.contains(role_current) { if !member.roles.contains(role_current) {
roles_set.current_add += 1; roles_set[1] += 1;
roles.push(role_current.to_owned()); roles.push(role_current.to_owned());
} }
if let Err(e) = member.add_roles(ctx, &roles).await { if let Err(e) = member.add_roles(ctx, &roles).await {
println!("{e:?}"); println!("{:?}", e);
} }
} else { } else {
// old and never // old and never
@ -81,16 +65,16 @@ pub mod normal {
} }
if member.roles.contains(role_current) { if member.roles.contains(role_current) {
roles_set.current_rem += 1; roles_set[2] += 1;
// if they're not a current member and have the role then remove it // if theya re not a current member and have the role then remove it
if let Err(e) = member.remove_role(ctx, role_current).await { if let Err(e) = member.remove_role(ctx, role_current).await {
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
for role in remove_roles.iter().flatten() { for role in remove_roles.iter().flatten() {
if let Err(e) = member.remove_role(ctx, role).await { if let Err(e) = member.remove_role(ctx, role).await {
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -99,14 +83,7 @@ pub mod normal {
set_server_numbers(&db, server, members_all as i64, members.len() as i64).await; set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;
// small bit of logging to note changes over time // small bit of logging to note changes over time
println!( println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.get(), roles_set[0], roles_set[1], roles_set[2]);
"{:?} 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<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> { pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
@ -146,7 +123,7 @@ pub mod normal {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("Failure to insert into {}", server.get()); println!("Failure to insert into {}", server.get());
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -154,29 +131,28 @@ pub mod normal {
// for updating committee members // for updating committee members
pub mod committee { pub mod committee {
use crate::{ use crate::common::database::{DataBase, Wolves};
common::{ use crate::common::wolves::committees::Committees;
database::{get_channel_from_row, get_role_from_row, DataBase, Wolves}, use crate::Config;
wolves::committees::Committees, use serenity::all::EditRole;
}, use serenity::builder::CreateChannel;
Config, use serenity::client::Context;
}; use serenity::model::channel::ChannelType;
use serde::{Deserialize, Serialize}; use serenity::model::guild::Member;
use serenity::{ use serenity::model::id::ChannelId;
all::EditRole, use serenity::model::prelude::RoleId;
builder::CreateChannel, use sqlx::{Pool, Sqlite};
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::collections::HashMap;
use std::sync::Arc;
pub async fn check_committee(ctx: &Context) { pub async fn check_committee(ctx: Arc<Context>) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone() data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
@ -188,48 +164,55 @@ pub mod committee {
// because to use it to update a single user we need to pre-get the members of teh 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(); 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 them the appropriate roles on teh committee server This function can take a vec of members (or just one) and gives tehm the appropiate roles on teh committee server
*/ */
pub async fn update_committees(db: &Pool<Sqlite>, ctx: &Context, config: &Config, members: &mut Vec<Member>) { pub async fn update_committees(db: &Pool<Sqlite>, ctx: &Context, config: &Config, members: &mut Vec<Member>) {
let server = config.committee_server; let server = config.committee_server;
let committee_member = config.committee_role; let committee_member = RoleId::new(1226602779968274573);
let committees = match get_committees(db).await { let committees = get_committees(db).await;
None => { let categories = vec![
return; // C&S Chats 1
} ChannelId::new(1226606560973815839),
Some(x) => x, // C&S Chats 2
}; ChannelId::new(1341457244973305927),
let categories = config.committee_category.clone(); // C&S Chats 3
ChannelId::new(1341457509717639279),
];
// information about the server // information about the server
let mut roles_db = HashMap::new(); let roles = server.roles(&ctx).await.unwrap_or_default();
for role in db_roles_get(db).await { let channels = server.channels(&ctx).await.unwrap_or_default();
roles_db.insert(
role.id_wolves, // make a hashmap of the nameof roles to quickly get them out again
CommitteeRoles { let mut roles_name = HashMap::new();
id_wolves: role.id_wolves, for role in roles.values() {
id_role: role.id_role, roles_name.insert(role.name.to_owned(), role.to_owned());
id_channel: role.id_channel,
name_role: role.name_role,
name_channel: role.name_channel,
// always start at 0
count: 0,
},
);
} }
let mut channels = server.channels(&ctx).await.unwrap_or_default(); let mut channels_name = HashMap::new();
for channel in channels.values() {
// we only care about teh channels in teh category
if let Some(x) = channel.parent_id {
for category in &categories {
if x.eq(category) {
channels_name.insert(channel.name.to_owned(), channel.to_owned());
}
}
}
}
// a map of users and the roles they are going to be getting // a map of users and the roles they are goign to be getting
let mut users_roles = HashMap::new(); let mut users_roles = HashMap::new();
let mut re_order = false; // a list of all the roles that can be removed from folks who should have them
// we need to create roles and channels if they don't already exist let mut committee_roles = vec![committee_member];
let mut category_index = 0; let mut category_index = 0;
let mut i = 0; let mut i = 0;
loop { loop {
if i >= committees.len() { if i >= committees.len() {
@ -237,17 +220,24 @@ pub mod committee {
} }
let committee = &committees[i]; let committee = &committees[i];
// if a club/soc ever changes their name // get the role for this committee/club/soc
if let Some(x) = roles_db.get_mut(&committee.id) { let role = match roles_name.get(&committee.name_full) {
committee.name_full.clone_into(&mut x.name_role); Some(x) => Some(x.to_owned()),
committee.name_profile.clone_into(&mut x.name_channel); None => {
} // create teh role if it does not exist
match server
.create_role(&ctx, EditRole::new().name(&committee.name_full).hoist(false).mentionable(true))
.await
{
Ok(x) => Some(x),
Err(_) => None,
}
}
};
// handle new clubs/socs // create teh channel if it does nto exist
if let std::collections::hash_map::Entry::Vacant(e) = roles_db.entry(committee.id) { if !channels_name.contains_key(&committee.name_profile) {
// create channel match server
// channel is first as the categories can only contain 50 channels
let channel = match server
.create_channel( .create_channel(
&ctx, &ctx,
CreateChannel::new(&committee.name_profile) CreateChannel::new(&committee.name_profile)
@ -257,93 +247,59 @@ pub mod committee {
.await .await
{ {
Ok(x) => { Ok(x) => {
println!("Created channel: {}", &committee.name_profile); // update teh channels name list
channels_name.insert(x.name.to_owned(), x.to_owned());
x.id println!("Created channel: {}", &committee.name_profile);
} }
Err(x) => { Err(x) => {
let tmp = x.to_string(); let tmp = x.to_string();
dbg!("Unable to create channel: ", &tmp, &tmp.contains("Maximum number of channels in category reached (50)"));
if x.to_string().contains("Maximum number of channels in category reached (50)") { if x.to_string().contains("Maximum number of channels in category reached (50)") {
category_index += 1; category_index += 1;
continue; continue;
} }
ChannelId::new(1) dbg!("Unable to create channel: ", &tmp, &tmp.contains("Maximum number of channels in category reached (50)"));
} }
}; }
};
// create role // so if the role exists
let role = match server if let Some(r) = role {
.create_role(&ctx, EditRole::new().name(&committee.name_full).hoist(false).mentionable(true)) committee_roles.push(r.id);
.await
{
Ok(x) => x.id,
Err(_) => RoleId::new(1),
};
let tmp = CommitteeRoles { for id_wolves in &committee.committee {
id_wolves: committee.id, // ID in this is the wolves ID, so we need to get a matching discord ID (if one exists)
id_role: role, if let Some(x) = get_server_member_discord(db, id_wolves).await {
id_channel: channel, if let Some(member_tmp) = x.discord {
name_role: committee.name_full.to_owned(), let values = users_roles.entry(member_tmp).or_insert(vec![committee_member]);
name_channel: committee.name_profile.to_owned(), values.push(r.id);
count: 0, }
}; }
}
// save it to the db in case of crash or error
db_role_set(db, &tmp).await;
// insert it into teh local cache
e.insert(tmp);
re_order = true;
} }
i += 1; i += 1;
} }
for committee in &committees {
let r = if let Some(x) = roles_db.get(&committee.id) {
x.id_role
} else {
continue;
};
for id_wolves in &committee.committee {
// ID in this is the wolves ID, so we need to get a matching discord ID (if one exists)
if let Some(x) = get_server_member_discord(db, id_wolves).await {
if let Some(member_tmp) = x.discord {
if server.member(ctx, &member_tmp).await.is_ok() {
let values = users_roles.entry(member_tmp).or_insert(vec![]);
values.push(r);
if let Some(x) = roles_db.get_mut(&committee.id) {
x.count += 1;
}
}
}
}
}
}
// now we have a map of all users that should get roles time to go through all the folks on teh server // 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 { for member in members {
// if member.user.id != 136522490632601600 {
// continue;
// }
//
let roles_current = member.roles(ctx).unwrap_or_default(); let roles_current = member.roles(ctx).unwrap_or_default();
let roles_required = match users_roles.get(&member.user.id) { let roles_required = match users_roles.get(&member.user.id) {
None => { None => {
vec![] vec![]
} }
Some(x) => x.to_owned(), Some(x) => {
let mut tmp = x.to_owned();
if !tmp.is_empty() {
tmp.push(committee_member);
}
tmp
}
}; };
let on_committee = !roles_required.is_empty();
let mut roles_rem = vec![]; let mut roles_rem = vec![];
let mut roles_add = vec![]; let mut roles_add = vec![];
// get a list of all the roles to remove from someone // get a list of all the roles to remove from someone
@ -352,28 +308,13 @@ pub mod committee {
for role in &roles_current { for role in &roles_current {
roles_current_id.push(role.id.to_owned()); roles_current_id.push(role.id.to_owned());
if !roles_required.contains(&role.id) { if !roles_required.contains(&role.id) {
if role.id == committee_member && on_committee {
continue;
}
roles_rem.push(role.id.to_owned()); roles_rem.push(role.id.to_owned());
} }
} }
let has_committee_role = roles_current_id.contains(&committee_member);
if on_committee && !has_committee_role {
// if there are committee roles then give the general purpose role
roles_add.push(committee_member);
}
if !on_committee && has_committee_role {
roles_rem.push(committee_member);
}
if !roles_required.is_empty() { if !roles_required.is_empty() {
if let Some(x) = roles_db.get_mut(&0) { // if there are committee roles then give the general purporse role
x.count += 1; roles_add.push(committee_member);
}
} }
for role in &roles_required { for role in &roles_required {
@ -389,118 +330,40 @@ pub mod committee {
if !roles_add.is_empty() { if !roles_add.is_empty() {
// these roles are flavor roles, only there to make folks mentionable // these roles are flavor roles, only there to make folks mentionable
member.add_roles(&ctx, &roles_add).await.unwrap_or_default(); member.add_roles(&ctx, &roles_add).await.unwrap_or_default();
} else {
member.remove_roles(&ctx, &[committee_member]).await.unwrap_or_default();
} }
} }
let mut channel_names = vec![]; // finally re-order teh channels to make them visually apealing
let mut positions = vec![]; let mut channel_names = channels_name.clone().into_keys().collect::<Vec<String>>();
for role in roles_db.values() { channel_names.sort();
// save these to db
db_role_set(db, role).await;
if re_order { // get a list of all teh new positions
let channel_id = role.id_channel.to_owned(); let mut new_positions = vec![];
if let Some(channel) = channels.get_mut(&channel_id) { for (i, name) in channel_names.iter().enumerate() {
// record the position of each of teh C&S channels if let Some(channel) = channels_name.get_mut(name) {
positions.push(channel.position); let position_new = i as u64;
if position_new != channel.position as u64 {
// pull out teh channel names new_positions.push((channel.id.to_owned(), position_new));
channel_names.push((role.name_channel.to_owned(), channel_id));
} }
} }
} }
if re_order { if !new_positions.is_empty() {
// sort by the position and name match server.reorder_channels(&ctx, new_positions).await {
positions.sort(); Ok(_) => {
channel_names.sort_by_key(|(name, _)| name.to_owned()); println!("Successfully re-orderd the committee category");
}
let mut new_positions = vec![]; Err(e) => {
for (i, (_, id)) in channel_names.iter().enumerate() { dbg!("Failed to re-order ", e);
new_positions.push((id.to_owned(), positions[i] as u64));
}
if !new_positions.is_empty() {
match server.reorder_channels(&ctx, new_positions).await {
Ok(_) => {
println!("Successfully re-orderd the committee category");
}
Err(e) => {
dbg!("Failed to re-order ", e);
}
} }
} }
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)] async fn get_committees(db: &Pool<Sqlite>) -> Vec<Committees> {
pub struct CommitteeRoles { sqlx::query_as::<_, Committees>(
id_wolves: i64,
pub id_role: RoleId,
pub id_channel: ChannelId,
pub name_role: String,
pub name_channel: String,
pub count: i64,
}
impl<'r> FromRow<'r, SqliteRow> for CommitteeRoles {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
id_role: get_role_from_row(row, "id_role"),
id_channel: get_channel_from_row(row, "id_channel"),
name_role: row.try_get("name_role")?,
name_channel: row.try_get("name_channel")?,
count: row.try_get("count")?,
})
}
}
async fn db_role_set(db: &Pool<Sqlite>, role: &CommitteeRoles) {
// expiry
match sqlx::query_as::<_, CommitteeRoles>(
"
INSERT INTO committee_roles (id_wolves, id_role, id_channel, name_role, name_channel, count)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT(id_wolves) DO UPDATE SET name_role = $4, name_channel = $5, count = $6
",
)
.bind(role.id_wolves)
.bind(role.id_role.get() as i64)
.bind(role.id_channel.get() as i64)
.bind(&role.name_role)
.bind(&role.name_channel)
.bind(role.count)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {role:?}");
println!("{e:?}");
}
}
}
pub async fn db_roles_get(db: &Pool<Sqlite>) -> Vec<CommitteeRoles> {
// expiry
sqlx::query_as::<_, CommitteeRoles>(
"
SELECT *
FROM committee_roles
",
)
.fetch_all(db)
.await
.unwrap_or_else(|e| {
println!("Failure to get Roles from committee_roles");
println!("{e:?}");
vec![]
})
}
pub async fn get_committees(db: &Pool<Sqlite>) -> Option<Vec<Committees>> {
match sqlx::query_as::<_, Committees>(
r#" r#"
SELECT * SELECT *
FROM committees FROM committees
@ -508,13 +371,10 @@ pub mod committee {
) )
.fetch_all(db) .fetch_all(db)
.await .await
{ .unwrap_or_else(|e| {
Ok(x) => Some(x), dbg!(e);
Err(e) => { vec![]
dbg!(e); })
None
}
}
} }
async fn get_server_member_discord(db: &Pool<Sqlite>, user: &i64) -> Option<Wolves> { async fn get_server_member_discord(db: &Pool<Sqlite>, user: &i64) -> Option<Wolves> {

View file

@ -38,8 +38,8 @@ async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesResultUserMin) {
{ {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("Failure to insert into Wolves {user:?}"); println!("Failure to insert into Wolves {:?}", user);
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -48,14 +48,12 @@ async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesResultUserMin) {
This is getting data for Clubs and Socs This is getting data for Clubs and Socs
*/ */
pub mod cns { pub mod cns {
use crate::{ use crate::common::database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers};
common::{ use crate::common::set_roles::normal::update_server;
database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers}, use crate::common::wolves::{add_users_wolves, WolvesResultUserMin};
wolves::{add_users_wolves, WolvesResultUserMin}, use crate::Config;
}, use serenity::client::Context;
Config, use serenity::model::id::GuildId;
};
use serenity::{client::Context, model::id::GuildId};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -69,10 +67,11 @@ pub mod cns {
} }
pub async fn get_wolves(ctx: &Context) { pub async fn get_wolves(ctx: &Context) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
@ -88,7 +87,7 @@ pub mod cns {
server, server,
// this is the unique api key for each club/soc // this is the unique api key for each club/soc
wolves_api, wolves_api,
wolves_id, server_name,
.. ..
} = &server_config; } = &server_config;
// dbg!(&server_config); // dbg!(&server_config);
@ -97,11 +96,12 @@ pub mod cns {
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>(); let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// list of users that need to be updated for this server // list of users that need to be updated for this server
let mut user_to_update = vec![];
let mut server_name_tmp = None; let mut server_name_tmp = None;
for user in wolves.get_members(wolves_api).await { for user in wolves.get_members(wolves_api).await {
// dbg!(&user.committee); // dbg!(&user.committee);
if server_name_tmp.is_none() { if server_name_tmp.is_none() {
server_name_tmp = Some(user.committee_id); server_name_tmp = Some(user.committee.to_owned());
} }
let id = user.member_id.parse::<u64>().unwrap_or_default(); let id = user.member_id.parse::<u64>().unwrap_or_default();
match existing.get(&(id as i64)) { match existing.get(&(id as i64)) {
@ -115,28 +115,35 @@ pub mod cns {
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await; add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
if old.expiry != user.expiry { if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await; add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
} }
} }
} }
} }
if let Some(cs_id) = server_name_tmp { if let Some(name) = server_name_tmp {
if &cs_id != wolves_id { if &name != server_name {
set_server_member(&db, server, cs_id).await; set_server_member(&db, server, &name).await;
} }
} }
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
} }
} }
async fn set_server_member(db: &Pool<Sqlite>, server: &GuildId, wolves_id: i64) { async fn set_server_member(db: &Pool<Sqlite>, server: &GuildId, name: &str) {
match sqlx::query_as::<_, Servers>( match sqlx::query_as::<_, Servers>(
" "
UPDATE servers UPDATE servers
SET wolves_id = ? SET server_name = ?
WHERE server = ? WHERE server = ?
", ",
) )
.bind(wolves_id) .bind(name)
.bind(server.get() as i64) .bind(server.get() as i64)
.fetch_optional(db) .fetch_optional(db)
.await .await
@ -144,7 +151,7 @@ pub mod cns {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("Failure to set server name {}", server.get()); println!("Failure to set server name {}", server.get());
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -183,7 +190,7 @@ pub mod cns {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.get(), user); println!("Failure to insert into ServerMembers {} {:?}", server.get(), user);
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }
@ -193,7 +200,8 @@ pub mod cns {
Get and store the data on C&S committees Get and store the data on C&S committees
*/ */
pub mod committees { pub mod committees {
use crate::{common::database::DataBase, Config}; use crate::common::database::DataBase;
use crate::Config;
use serenity::client::Context; use serenity::client::Context;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
@ -223,10 +231,11 @@ pub mod committees {
} }
pub async fn get_cns(ctx: &Context) { pub async fn get_cns(ctx: &Context) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
@ -261,8 +270,8 @@ pub mod committees {
{ {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("Failure to insert into Committees {committee:?}"); println!("Failure to insert into Committees {:?}", committee);
println!("{e:?}"); println!("{:?}", e);
} }
} }
} }

View file

@ -3,10 +3,10 @@ pub mod common;
use chrono::{Datelike, SecondsFormat, Utc}; use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv; use dotenvy::dotenv;
use rand::{distr::Alphanumeric, rng, Rng}; use rand::{distr::Alphanumeric, rng, Rng};
use serenity::{ use serenity::all::CommandInteraction;
model::id::{ChannelId, GuildId, RoleId}, use serenity::client::Context;
prelude::TypeMapKey, use serenity::model::id::{ChannelId, GuildId, RoleId};
}; use serenity::prelude::TypeMapKey;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -34,10 +34,7 @@ pub struct Config {
// discord server for committee // discord server for committee
pub committee_server: GuildId, pub committee_server: GuildId,
pub committee_role: RoleId, pub committee_role: RoleId,
pub committee_category: Vec<ChannelId>, pub committee_category: ChannelId,
// items pertaining to CompSoc only
pub compsoc_server: GuildId,
} }
impl TypeMapKey for Config { impl TypeMapKey for Config {
type Value = Arc<RwLock<Config>>; type Value = Arc<RwLock<Config>>;
@ -62,8 +59,7 @@ pub fn get_config() -> Config {
wolves_api: "".to_string(), wolves_api: "".to_string(),
committee_server: GuildId::new(1), committee_server: GuildId::new(1),
committee_role: RoleId::new(1), committee_role: RoleId::new(1),
committee_category: vec![], committee_category: ChannelId::new(1),
compsoc_server: GuildId::new(1),
}; };
if let Ok(x) = env::var("DATABASE_HOME") { if let Ok(x) = env::var("DATABASE_HOME") {
@ -105,22 +101,14 @@ pub fn get_config() -> Config {
config.committee_server = GuildId::new(x); config.committee_server = GuildId::new(x);
} }
} }
if let Ok(x) = env::var("COMMITTEE_ROLE") { if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() { if let Ok(x) = x.trim().parse::<u64>() {
config.committee_role = RoleId::new(x); config.committee_role = RoleId::new(x);
} }
} }
if let Ok(x) = env::var("COMMITTEE_CATEGORY") { if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
for part in x.split(',') {
if let Ok(x) = part.trim().parse::<u64>() {
config.committee_category.push(ChannelId::new(x));
}
}
}
if let Ok(x) = env::var("COMPSOC_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() { if let Ok(x) = x.trim().parse::<u64>() {
config.compsoc_server = GuildId::new(x); config.committee_category = ChannelId::new(x);
} }
} }
@ -139,3 +127,41 @@ pub fn get_now_iso(short: bool) -> String {
pub fn random_string(len: usize) -> String { pub fn random_string(len: usize) -> String {
rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
} }
/**
For any time ye need to check if a user who calls a command has admin privlages
*/
pub async fn is_admin(command: &CommandInteraction, ctx: &Context) -> Option<String> {
let mut admin = false;
let g_id = match command.guild_id {
None => return Some("Not in a server".to_string()),
Some(x) => x,
};
let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();
if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
if let Some(permissions) = member.permissions {
if permissions.administrator() {
admin = true;
}
}
for role_id in member.roles {
if admin {
break;
}
if let Some(role) = roles_server.get(&role_id) {
if role.permissions.administrator() {
admin = true;
}
}
}
}
if !admin {
Some("Administrator permission required".to_string())
} else {
None
}
}

View file

@ -1,28 +1,21 @@
pub mod commands; pub mod commands;
use crate::commands::role_adder::tools::on_role_change; use crate::commands::role_adder::tools::on_role_change;
use serenity::all::{ActivityData, Command, CreateMessage, EditInteractionResponse, GuildMemberUpdateEvent, Interaction};
use serenity::model::guild::Member;
use serenity::{ use serenity::{
all::{Command, CommandDataOptionValue, CreateMessage, EditInteractionResponse, Interaction},
async_trait, async_trait,
client::{Context, EventHandler}, client::{Context, EventHandler},
gateway::{ActivityData, ChunkGuildFilter},
model::{ model::{
event::GuildMemberUpdateEvent,
gateway::{GatewayIntents, Ready}, gateway::{GatewayIntents, Ready},
guild::Member,
id::GuildId,
user::OnlineStatus, user::OnlineStatus,
}, },
Client, Client,
}; };
use skynet_discord_bot::{ use skynet_discord_bot::common::database::{db_init, get_server_config, get_server_member, DataBase};
common::{ use skynet_discord_bot::common::set_roles::committee::update_committees;
database::{db_init, get_server_config, get_server_member, DataBase}, use skynet_discord_bot::common::wolves::committees::Committees;
set_roles::committee::update_committees, use skynet_discord_bot::{get_config, Config};
wolves::committees::Committees,
},
get_config, Config,
};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -31,21 +24,15 @@ struct Handler;
#[async_trait] #[async_trait]
impl EventHandler for Handler { impl EventHandler for Handler {
// this caches members of all servers teh bot is in
async fn cache_ready(&self, ctx: Context, guilds: Vec<GuildId>) {
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 // handles previously linked accounts joining the server
async fn guild_member_addition(&self, ctx: Context, new_member: Member) { async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
}; };
let db = db_lock.read().await;
let config_lock = { let config_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone() data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
@ -53,8 +40,7 @@ impl EventHandler for Handler {
let config_global = config_lock.read().await; let config_global = config_lock.read().await;
// committee server takes priority // committee server takes priority
let committee_server = config_global.committee_server; if new_member.guild_id.eq(&config_global.committee_server) {
if new_member.guild_id.get() == committee_server.get() {
let mut member = vec![new_member.clone()]; let mut member = vec![new_member.clone()];
update_committees(&db, &ctx, &config_global, &mut member).await; update_committees(&db, &ctx, &config_global, &mut member).await;
return; return;
@ -79,16 +65,16 @@ impl EventHandler for Handler {
} }
if let Err(e) = new_member.add_roles(&ctx, &roles).await { if let Err(e) = new_member.add_roles(&ctx, &roles).await {
println!("{e:?}"); println!("{:?}", e);
} }
} else { } else {
let tmp = get_committee(&db, config_server.wolves_id).await; let tmp = get_committee(&db, &config_server.server_name).await;
if !tmp.is_empty() { if !tmp.is_empty() {
let committee = &tmp[0]; let committee = &tmp[0];
let msg = format!( let msg = format!(
r#" r#"
Welcome {} to the {} server! Welcome {} to the {} server!
Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/wolves link email_here`` with the email associated with your wolves account, to get full access. Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/link_wolves`` to get full access.
"#, "#,
new_member.display_name(), new_member.display_name(),
committee.name_full, committee.name_full,
@ -107,12 +93,14 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
// handles role updates // handles role updates
async fn guild_member_update(&self, ctx: Context, _old_data: Option<Member>, new_data: Option<Member>, _: GuildMemberUpdateEvent) { async fn guild_member_update(&self, ctx: Context, _old_data: Option<Member>, new_data: Option<Member>, _: GuildMemberUpdateEvent) {
// get config/db // get config/db
let db = { let db_lock = {
let data_read = ctx.data.read().await; let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone() data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
}; };
// check if the role changed is part of the ones for this server let db = db_lock.read().await;
// check if the role changed is part of the oens for this server
if let Some(x) = new_data { if let Some(x) = new_data {
on_role_change(&db, &ctx, x).await; on_role_change(&db, &ctx, x).await;
} }
@ -122,54 +110,24 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
println!("[Main] {} is connected!", ready.user.name); println!("[Main] {} is connected!", ready.user.name);
ctx.set_presence(Some(ActivityData::playing("with humanity's fate")), OnlineStatus::Online); 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::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
match Command::set_global_commands( match Command::set_global_commands(
&ctx.http, &ctx.http,
vec![ vec![
commands::wolves::register(), commands::add_server::register(),
commands::committee::register(), commands::role_adder::edit::register(),
commands::link_email::link::register(),
commands::link_email::verify::register(),
commands::minecraft::server::add::register(), commands::minecraft::server::add::register(),
commands::minecraft::server::list::register(), commands::minecraft::server::list::register(),
commands::minecraft::server::delete::register(), commands::minecraft::server::delete::register(),
commands::minecraft::user::add::register(),
], ],
) )
.await .await
{ {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
println!("{e:?}") println!("{:?}", e)
}
}
// Inter-Committee server
match config.committee_server.set_commands(&ctx.http, vec![commands::count::committee::register()]).await {
Ok(_) => {}
Err(e) => {
println!("{e:?}")
}
}
// CompSoc Server
match config
.compsoc_server
.set_commands(
&ctx.http,
vec![
// commands just for the CompSoc server
commands::count::servers::register(),
commands::server_icon::user::register(),
],
)
.await
{
Ok(_) => {}
Err(e) => {
println!("{e:?}")
} }
} }
} }
@ -177,100 +135,38 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
async fn interaction_create(&self, ctx: Context, interaction: Interaction) { async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction { if let Interaction::Command(command) = interaction {
let _ = command.defer_ephemeral(&ctx.http).await; 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() { let content = match command.data.name.as_str() {
// user commands // user commands
"wolves" => match command.data.options.first() { "link_wolves" => commands::link_email::link::run(&command, &ctx).await,
None => "Invalid Command".to_string(), "verify" => commands::link_email::verify::run(&command, &ctx).await,
Some(x) => match x.name.as_str() { "link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await,
"link" => commands::wolves::link::run(&command, &ctx).await,
"verify" => commands::wolves::verify::run(&command, &ctx).await,
"unlink" => commands::wolves::unlink::run(&command, &ctx).await,
"link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await,
"docs" => commands::wolves::link_docs::users::run(&command, &ctx).await,
// "link" => commands::count::servers::run(&command, &ctx).await,
&_ => format!("not implemented :( wolves {}", x.name.as_str()),
},
},
// admin commands // admin commands
"committee" => match command.data.options.first() { "add" => commands::add_server::run(&command, &ctx).await,
None => "Invalid Command".to_string(), "roles_adder" => commands::role_adder::edit::run(&command, &ctx).await,
Some(x) => match x.name.as_str() {
"add" => commands::add_server::run(&command, &ctx).await,
"roles_adder" => commands::role_adder::edit::run(&command, &ctx).await,
"icon" => match &x.value {
CommandDataOptionValue::SubCommandGroup(y) => match y.first() {
None => "error".to_string(),
Some(z) => match z.name.as_str() {
"change" => commands::server_icon::admin::change::run(&command, &ctx).await,
&_ => format!("not implemented :( count {}", x.name.as_str()),
},
},
_ => {
format!("not implemented :( committee {}", x.name.as_str())
}
},
// TODO: move teh minecraft commands in here as a subgroup
// "link" => commands::count::servers::run(&command, &ctx).await,
&_ => format!("not implemented :( committee {}", x.name.as_str()),
},
},
"minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await, "minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await,
"minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await, "minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await,
"minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await, "minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await,
// sub command _ => "not implemented :(".to_string(),
"count" => match command.data.options.first() {
None => "Invalid Command".to_string(),
Some(x) => match x.name.as_str() {
"committee" => commands::count::committee::run(&command, &ctx).await,
"servers" => commands::count::servers::run(&command, &ctx).await,
&_ => format!("not implemented :( count {}", x.name.as_str()),
},
},
"icon" => match command.data.options.first() {
None => "Invalid Command".to_string(),
Some(x) => match x.name.as_str() {
"current" => {
let result = match &x.value {
CommandDataOptionValue::SubCommandGroup(y) => match y.first() {
None => "error".to_string(),
Some(z) => match z.name.as_str() {
"icon" => commands::server_icon::user::current::icon::run(&command, &ctx).await,
"festival" => commands::server_icon::user::current::festival::run(&command, &ctx).await,
&_ => format!("not implemented :( count {}", x.name.as_str()),
},
},
&_ => format!("not implemented :( {}", command.data.name.as_str()),
};
result
}
"stats" => commands::server_icon::user::stats::run(&command, &ctx).await,
&_ => format!("not implemented :( count {}", x.name.as_str()),
},
},
_ => format!("not implemented :( {}", command.data.name.as_str()),
}; };
if let Err(why) = command.edit_response(&ctx.http, EditInteractionResponse::new().content(content)).await { 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);
} }
} }
} }
} }
async fn get_committee(db: &Pool<Sqlite>, wolves_id: i64) -> Vec<Committees> { async fn get_committee(db: &Pool<Sqlite>, committee: &str) -> Vec<Committees> {
sqlx::query_as::<_, Committees>( sqlx::query_as::<_, Committees>(
r#" r#"
SELECT * SELECT *
FROM committees FROM committees
WHERE id = ? WHERE name_plain = ?
"#, "#,
) )
.bind(wolves_id) .bind(committee)
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap_or_default() .unwrap_or_default()
@ -291,8 +187,7 @@ async fn main() {
let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS; let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS;
// Build our client. // Build our client.
let mut client = Client::builder(&config.discord_token, intents) let mut client = Client::builder(&config.discord_token, intents)
.event_handler(Handler) .event_handler(Handler {})
.cache_settings(serenity::cache::Settings::default())
.await .await
.expect("Error creating client"); .expect("Error creating client");
@ -300,7 +195,7 @@ async fn main() {
let mut data = client.data.write().await; let mut data = client.data.write().await;
data.insert::<Config>(Arc::new(RwLock::new(config))); data.insert::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(Arc::new(db)); data.insert::<DataBase>(Arc::new(RwLock::new(db)));
} }
// Finally, start a single shard, and start listening to events. // Finally, start a single shard, and start listening to events.
@ -308,6 +203,6 @@ async fn main() {
// Shards will automatically attempt to reconnect, and will perform // Shards will automatically attempt to reconnect, and will perform
// exponential backoff until it reconnects. // exponential backoff until it reconnects.
if let Err(why) = client.start().await { if let Err(why) = client.start().await {
println!("Client error: {why:?}"); println!("Client error: {:?}", why);
} }
} }