diff --git a/.env.sample b/.env.sample
index 61c4d8c..13e0d73 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,5 +1,5 @@
DISCORD_TOKEN=
ROOT_URL=https://root.amfoss.in/
OWNER_ID=
-AMD_RUST_ENV=trace
+DEBUG=true
ENABLE_DEBUG_LIBRARIES=false
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7df5f5d..be30283 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -7,7 +7,12 @@ updates:
open-pull-requests-limit: 1
target-branch: "develop"
groups:
- all-dependencies:
- applies-to: [version-updates, security-updates]
+ version-updates:
+ applies-to: "version-updates"
+ patterns:
+ - "*"
+
+ security-updates:
+ applies-to: "security-updates"
patterns:
- "*"
diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml
index 40092b3..f933705 100644
--- a/.github/workflows/deploy_docs.yml
+++ b/.github/workflows/deploy_docs.yml
@@ -3,7 +3,7 @@ name: Deploy Rust Docs
on:
push:
branches:
- - main
+ - production
jobs:
deploy:
@@ -23,4 +23,3 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc
-
diff --git a/.github/workflows/generate-release.yml b/.github/workflows/generate-release.yml
index 720b29c..ae21b3b 100644
--- a/.github/workflows/generate-release.yml
+++ b/.github/workflows/generate-release.yml
@@ -43,6 +43,11 @@ jobs:
with:
targets: ${{ matrix.target }}
+ - name: Cache Rust build
+ uses: Swatinem/rust-cache@v2
+ with:
+ key: ${{ matrix.target }}
+
- name: Build binary
run: cargo build --release --target ${{ matrix.target }}
env:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 359e847..491cc23 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,7 +2,7 @@ name: Lint
on:
pull_request:
- branches: [ "main", "develop" ]
+ branches: ["production", "develop"]
jobs:
clippy:
diff --git a/Cargo.lock b/Cargo.lock
index 1bef406..d2370d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "addr2line"
@@ -28,7 +28,7 @@ dependencies = [
[[package]]
name = "amd"
-version = "1.2.1"
+version = "1.3.0"
dependencies = [
"anyhow",
"async-trait",
@@ -36,7 +36,7 @@ dependencies = [
"chrono-tz",
"dotenv",
"poise",
- "reqwest 0.12.12",
+ "reqwest 0.12.23",
"serde",
"serde_json",
"serenity",
@@ -45,12 +45,6 @@ dependencies = [
"tracing-subscriber",
]
-[[package]]
-name = "android-tzdata"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
-
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -62,9 +56,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.95"
+version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arrayvec"
@@ -77,9 +71,9 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.85"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
@@ -218,40 +212,28 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.39"
+version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
- "android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
- "windows-targets 0.52.6",
+ "windows-link 0.2.0",
]
[[package]]
name = "chrono-tz"
-version = "0.10.1"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f"
+checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
dependencies = [
"chrono",
- "chrono-tz-build",
"phf",
]
-[[package]]
-name = "chrono-tz-build"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
-dependencies = [
- "parse-zoneinfo",
- "phf_codegen",
-]
-
[[package]]
name = "command_attr"
version = "0.5.3"
@@ -759,7 +741,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "socket2",
+ "socket2 0.5.8",
"tokio",
"tower-service",
"tracing",
@@ -768,9 +750,9 @@ dependencies = [
[[package]]
name = "hyper"
-version = "1.5.2"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
dependencies = [
"bytes",
"futures-channel",
@@ -808,7 +790,7 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
dependencies = [
"futures-util",
"http 1.2.0",
- "hyper 1.5.2",
+ "hyper 1.6.0",
"hyper-util",
"rustls 0.23.21",
"rustls-pki-types",
@@ -825,7 +807,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
"hyper-util",
"native-tls",
"tokio",
@@ -835,21 +817,28 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.10"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
+ "base64 0.22.1",
"bytes",
"futures-channel",
+ "futures-core",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
- "hyper 1.5.2",
+ "hyper 1.6.0",
+ "ipnet",
+ "libc",
+ "percent-encoding",
"pin-project-lite",
- "socket2",
+ "socket2 0.6.0",
+ "system-configuration 0.6.1",
"tokio",
"tower-service",
"tracing",
+ "windows-registry",
]
[[package]]
@@ -1030,12 +1019,33 @@ dependencies = [
"hashbrown 0.15.2",
]
+[[package]]
+name = "io-uring"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
+dependencies = [
+ "bitflags 2.8.0",
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "itoa"
version = "1.0.14"
@@ -1066,9 +1076,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
[[package]]
name = "libc"
-version = "0.2.169"
+version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "linux-raw-sys"
@@ -1100,11 +1110,11 @@ checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
@@ -1183,12 +1193,11 @@ dependencies = [
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -1265,12 +1274,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
-
[[package]]
name = "parking_lot"
version = "0.12.3"
@@ -1294,15 +1297,6 @@ dependencies = [
"windows-targets 0.52.6",
]
-[[package]]
-name = "parse-zoneinfo"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
-dependencies = [
- "regex",
-]
-
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1311,38 +1305,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
-dependencies = [
- "phf_shared",
-]
-
-[[package]]
-name = "phf_codegen"
-version = "0.11.3"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
dependencies = [
- "phf_generator",
"phf_shared",
]
-[[package]]
-name = "phf_generator"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
-dependencies = [
- "phf_shared",
- "rand",
-]
-
[[package]]
name = "phf_shared"
-version = "0.11.3"
+version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
dependencies = [
"siphasher",
]
@@ -1485,17 +1459,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -1506,15 +1471,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
]
-[[package]]
-name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -1546,7 +1505,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
- "rustls-pemfile 1.0.4",
+ "rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
@@ -1567,46 +1526,42 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.12.12"
+version = "0.12.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
+checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb"
dependencies = [
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-core",
- "futures-util",
"h2 0.4.7",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
- "hyper 1.5.2",
+ "hyper 1.6.0",
"hyper-rustls 0.27.5",
"hyper-tls",
"hyper-util",
- "ipnet",
"js-sys",
"log",
"mime",
"native-tls",
- "once_cell",
"percent-encoding",
"pin-project-lite",
- "rustls-pemfile 2.2.0",
+ "rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.2",
- "system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tower",
+ "tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "windows-registry",
]
[[package]]
@@ -1691,15 +1646,6 @@ dependencies = [
"base64 0.21.7",
]
-[[package]]
-name = "rustls-pemfile"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
-dependencies = [
- "rustls-pki-types",
-]
-
[[package]]
name = "rustls-pki-types"
version = "1.10.1"
@@ -1817,9 +1763,19 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.217"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
@@ -1835,9 +1791,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.217"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -1846,14 +1802,15 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.137"
+version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
+ "serde_core",
]
[[package]]
@@ -1977,6 +1934,16 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "spin"
version = "0.9.8"
@@ -2190,18 +2157,20 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.43.0"
+version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
"bytes",
+ "io-uring",
"libc",
"mio",
"pin-project-lite",
- "socket2",
+ "slab",
+ "socket2 0.6.0",
"tokio-macros",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -2300,6 +2269,24 @@ dependencies = [
"tower-service",
]
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags 2.8.0",
+ "bytes",
+ "futures-util",
+ "http 1.2.0",
+ "http-body 1.0.1",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -2358,14 +2345,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -2654,22 +2641,6 @@ dependencies = [
"rustls-pki-types",
]
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -2679,12 +2650,6 @@ dependencies = [
"windows-sys 0.59.0",
]
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -2695,33 +2660,44 @@ dependencies = [
]
[[package]]
-name = "windows-registry"
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
+ "windows-link 0.1.3",
"windows-result",
"windows-strings",
- "windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
-version = "0.2.0"
+version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
-version = "0.1.0"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
- "windows-result",
- "windows-targets 0.52.6",
+ "windows-link 0.1.3",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ede679c..241d9dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,19 +1,19 @@
[package]
name = "amd"
-version = "1.2.1"
+version = "1.3.0"
edition = "2021"
[dependencies]
-anyhow = "1.0.95"
-async-trait = "0.1.83"
-chrono = "0.4.38"
-chrono-tz = "0.10.0"
-reqwest = { version = "0.12.5", features = ["json"] }
-serde = { version = "1.0.203", features = ["derive"] }
-serde_json = "1.0.117"
-tokio = { version = "1.26.0", features = ["rt-multi-thread", "macros"] }
+anyhow = "1.0.100"
+async-trait = "0.1.89"
+chrono = "0.4.42"
+chrono-tz = "0.10.4"
+reqwest = { version = "0.12.23", features = ["json"] }
+serde = { version = "1.0.228", features = ["derive"] }
+serde_json = "1.0.145"
+tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
tracing = "0.1.37"
dotenv = "0.15.0"
serenity = { version = "0.12.4", features = ["chrono"] }
poise = "0.6.1"
-tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
+tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index e72aee3..2d19391 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -7,7 +7,7 @@ The rest of this document will explain the high-level details of the internals o
# Documentation
## Environment Variables
-`AMD_RUST_ENV`: Controls the log levels, although it can still be changed at runtime. Set to `production` to only log messages at the `INFO` level or above. If set to anything other than `production` say `dev`, tracing will also output logs to `stdout` as well to the file `amd.log`.
+`DEBUG`: Controls whether logs are printed to stdout. Set to `true` to only log messages into `amd.log`. If set to `false`, tracing will also output logs to `stdout` as well to `amd.log`.
`ENABLE_DEBUG_LIBRARIES`: Boolean that controls whether debug information from non-amd crates are logged.
`DISCORD_TOKEN`: The token for the bot.
`OWNER_ID`: The Discord User ID for a user that will be designated as the owner and will have access to certain privileged commands such as `set_log_level`.
diff --git a/docs/README.md b/docs/README.md
index 9ca70c7..6cbd2e5 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -11,7 +11,7 @@ If you want to contribute to `amD`, you'll likely need to run your own instance
- [Rust](https://www.rust-lang.org/tools/install)
- A Discord Bot Token from the [Discord Developer Protal](https://discord.com/developers/) .
-After which, you can make your changes to the source code and modify the environment variables to have your own instance up and running. A more detailed guide to development and contributing can be found in [CONTRIBUTING.md.](/docs/CONTRIBUTING.md)
+After which, you can make your changes to the source code and modify the environment variables to have your own instance up and running. A more detailed guide to development and contributing can be found in [CONTRIBUTING.md.](./docs/CONTRIBUTING.md)
# License
This project is licensed under the GNU General Public License v3.0. See the LICENSE file for details.
diff --git a/src/commands.rs b/src/commands.rs
deleted file mode 100644
index 85ed263..0000000
--- a/src/commands.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
-amFOSS Daemon: A discord bot for the amFOSS Discord server.
-Copyright (C) 2024 amFOSS
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-use anyhow::Context as _;
-use tracing::{info, trace};
-use tracing_subscriber::EnvFilter;
-
-use crate::{Context, Data, Error};
-
-#[poise::command(prefix_command)]
-async fn amdctl(ctx: Context<'_>) -> Result<(), Error> {
- trace!("Running amdctl command");
- ctx.say("amD is up and running.").await?;
- Ok(())
-}
-
-#[poise::command(prefix_command, owners_only)]
-async fn set_log_level(ctx: Context<'_>, level: String) -> Result<(), Error> {
- trace!("Running set_log_level command");
- let data = ctx.data();
- let reload_handle = data.log_reload_handle.write().await;
-
- let enable_debug_libraries_string = std::env::var("ENABLE_DEBUG_LIBRARIES")
- .context("ENABLE_DEBUG_LIBRARIES was not found in the ENV")?;
- let enable_debug_libraries: bool = enable_debug_libraries_string
- .parse()
- .context("Failed to parse ENABLE_DEBUG_LIBRARIES")?;
- let crate_name = env!("CARGO_CRATE_NAME");
- let new_filter = match level.to_lowercase().as_str() {
- "trace" => {
- if enable_debug_libraries {
- "trace".to_string()
- } else {
- format!("{crate_name}=trace")
- }
- }
- "debug" => {
- if enable_debug_libraries {
- "debug".to_string()
- } else {
- format!("{crate_name}=debug")
- }
- }
- "info" => {
- if enable_debug_libraries {
- "info".to_string()
- } else {
- format!("{crate_name}=info")
- }
- }
- "warn" => {
- if enable_debug_libraries {
- "warn".to_string()
- } else {
- format!("{crate_name}=warn")
- }
- }
- "error" => {
- if enable_debug_libraries {
- "error".to_string()
- } else {
- format!("{crate_name}=error")
- }
- }
- _ => {
- ctx.say("Invalid log level! Use: trace, debug, info, warn, error")
- .await?;
- return Ok(());
- }
- };
-
- if reload_handle.reload(EnvFilter::new(&new_filter)).is_ok() {
- ctx.say(format!("Log level changed to **{}**", new_filter))
- .await?;
- info!("Log level changed to {}", new_filter);
- } else {
- ctx.say("Failed to update log level.").await?;
- }
-
- Ok(())
-}
-
-/// Returns a vector containg [Poise Commands][`poise::Command`]
-pub fn get_commands() -> Vec> {
- vec![amdctl(), set_log_level()]
-}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
new file mode 100644
index 0000000..b114346
--- /dev/null
+++ b/src/commands/mod.rs
@@ -0,0 +1,37 @@
+mod set_log_level;
+
+use crate::commands::set_log_level::set_log_level;
+use serenity::all::RoleId;
+use tracing::{debug, instrument};
+
+use crate::{
+ ids::{FOURTH_YEAR_ROLE_ID, THIRD_YEAR_ROLE_ID},
+ Context, Data, Error,
+};
+
+/// Checks if the author has the Fourth Year or Third Year role. Can be used as an authorization procedure for other commands.
+#[allow(dead_code)]
+async fn is_privileged(ctx: &Context<'_>) -> bool {
+ if let Some(guild_id) = ctx.guild_id() {
+ if let Ok(member) = guild_id.member(ctx, ctx.author().id).await {
+ return member.roles.contains(&RoleId::new(FOURTH_YEAR_ROLE_ID))
+ || member.roles.contains(&RoleId::new(THIRD_YEAR_ROLE_ID));
+ }
+ }
+
+ false
+}
+
+#[poise::command(prefix_command)]
+#[instrument(level = "debug", skip(ctx))]
+async fn amdctl(ctx: Context<'_>) -> Result<(), Error> {
+ ctx.say("amD is up and running.").await?;
+ Ok(())
+}
+
+/// Returns a vector containg [Poise Commands][`poise::Command`]
+pub fn get_commands() -> Vec> {
+ let commands = vec![amdctl(), set_log_level()];
+ debug!(commands = ?commands.iter().map(|c| &c.name).collect::>());
+ commands
+}
diff --git a/src/commands/set_log_level.rs b/src/commands/set_log_level.rs
new file mode 100644
index 0000000..d2f24b0
--- /dev/null
+++ b/src/commands/set_log_level.rs
@@ -0,0 +1,70 @@
+/*
+amFOSS Daemon: A discord bot for the amFOSS Discord server.
+Copyright (C) 2024 amFOSS
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+//! Module for the set_log_level command.
+
+use crate::{Context, Error};
+use tracing::info;
+use tracing::instrument;
+use tracing_subscriber::EnvFilter;
+/// Returns whether the provided `level` String is a valid filter level for tracing.
+fn validate_level(level: &str) -> bool {
+ const VALID_LEVELS: [&str; 5] = ["trace", "debug", "info", "warn", "error"];
+ !VALID_LEVELS.contains(&level)
+}
+
+fn build_filter_string(level: String, enable_debug_libraries: bool) -> anyhow::Result {
+ let crate_name = env!("CARGO_CRATE_NAME");
+
+ if enable_debug_libraries {
+ Ok(level)
+ } else {
+ Ok(format!("{crate_name}={level}"))
+ }
+}
+
+#[poise::command(prefix_command, owners_only)]
+#[instrument(level = "debug", skip(ctx))]
+pub async fn set_log_level(
+ ctx: Context<'_>,
+ level: String,
+ enable_debug_libraries: Option,
+) -> Result<(), Error> {
+ if !validate_level(&level) {
+ ctx.say("Invalid log level! Use: trace, debug, info, warn, error")
+ .await?;
+ return Ok(());
+ }
+
+ let new_filter_level = build_filter_string(level, enable_debug_libraries.unwrap_or_default())?;
+
+ let data = ctx.data();
+ let reload_handle = data.log_reload_handle.write().await;
+
+ if reload_handle
+ .reload(EnvFilter::new(&new_filter_level))
+ .is_ok()
+ {
+ ctx.say(format!("Log level changed to **{new_filter_level}**"))
+ .await?;
+ info!("Log level changed to {}", new_filter_level);
+ } else {
+ ctx.say("Failed to update log level.").await?;
+ }
+
+ Ok(())
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..e8a4c30
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,75 @@
+/*
+amFOSS Daemon: A discord bot for the amFOSS Discord server.
+Copyright (C) 2024 amFOSS
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+use serenity::all::UserId;
+
+/// Environment variables for amD
+///
+/// # Fields
+///
+/// * debug: a boolean flag that decides in what context the application will be running on. When true, it is assumed to be in development. This allows us to filter out logs from `stdout` when in production. Defaults to false if not set.
+/// * enable_debug_libraries: Boolean flag that controls whether tracing will output logs from other crates used in the project. This is only needed for really serious bugs. Defaults to false if not set.
+/// * discord_token: The bot's discord token obtained from the Discord Developer Portal. The only mandatory variable required.
+/// * owner_id: Used to allow access to privileged commands to specific users. If not passed, will set the bot to have no owners.
+/// * prefix_string: The prefix used to issue commands to the bot on Discord. Always set to "$".
+pub struct Config {
+ pub debug: bool,
+ pub enable_debug_libraries: bool,
+ pub discord_token: String,
+ pub owner_id: Option,
+ pub prefix_string: String,
+ pub root_url: String,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ debug: parse_bool_env("DEBUG"),
+ enable_debug_libraries: parse_bool_env("ENABLE_DEBUG_LIBRARIES"),
+ discord_token: std::env::var("DISCORD_TOKEN")
+ .expect("DISCORD_TOKEN was not found in env"),
+ owner_id: parse_owner_id_env("OWNER_ID"),
+ prefix_string: String::from("$"),
+ root_url: std::env::var("ROOT_URL").expect("ROOT_URL was not found in env"),
+ }
+ }
+}
+
+/// Tries to access the environment variable through the key passed in. If it is set, it will try to parse it as u64 and if that fails, it will log the error and return the default value None. If it suceeds the u64 parsing, it will convert it to a UserId and return Some(UserId). If the env. var. is not set, it will return None.
+fn parse_owner_id_env(key: &str) -> Option {
+ std::env::var(key)
+ .ok()
+ .and_then(|s| {
+ s.parse::()
+ .map_err(|_| eprintln!("WARNING: Invalid OWNER_ID value '{s}', ignoring."))
+ .ok()
+ })
+ .map(UserId::new)
+}
+
+/// Tries to access the environment variable through the key passed in. If it is set but an invalid boolean, it will log an error through tracing and default to false. If it is not set, it will default to false.
+fn parse_bool_env(key: &str) -> bool {
+ std::env::var(key)
+ .map(|val| {
+ val.parse().unwrap_or_else(|_| {
+ eprintln!("Warning: Invalid DEBUG value '{val}', defaulting to false");
+ false
+ })
+ })
+ .unwrap_or(false)
+}
diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs
index d9c1786..5267712 100644
--- a/src/graphql/mod.rs
+++ b/src/graphql/mod.rs
@@ -17,3 +17,30 @@ along with this program. If not, see .
*/
pub mod models;
pub mod queries;
+
+use std::sync::Arc;
+
+use reqwest::Client;
+
+#[derive(Debug, Clone)]
+pub struct GraphQLClient {
+ http: Client,
+ root_url: Arc,
+}
+
+impl GraphQLClient {
+ pub fn new(root_url: String) -> Self {
+ Self {
+ http: Client::new(),
+ root_url: Arc::new(root_url),
+ }
+ }
+
+ pub fn root_url(&self) -> &str {
+ &self.root_url
+ }
+
+ pub fn http(&self) -> Client {
+ self.http.clone()
+ }
+}
diff --git a/src/graphql/models.rs b/src/graphql/models.rs
index 9444fa8..519e77f 100644
--- a/src/graphql/models.rs
+++ b/src/graphql/models.rs
@@ -18,21 +18,28 @@ along with this program. If not, see .
use serde::Deserialize;
#[derive(Clone, Debug, Deserialize)]
-pub struct StreakWithMemberId {
- #[serde(rename = "memberId")]
- pub member_id: i32,
- #[serde(rename = "currentStreak")]
- pub current_streak: i32,
- #[serde(rename = "maxStreak")]
- pub max_streak: i32,
+pub struct StatusOnDate {
+ #[serde(rename = "isSent")]
+ pub is_sent: bool,
+ #[serde(rename = "onBreak")]
+ pub on_break: bool,
}
#[derive(Clone, Debug, Deserialize)]
-pub struct Streak {
+pub struct StatusStreak {
#[serde(rename = "currentStreak")]
- pub current_streak: i32,
+ pub current_streak: Option,
#[serde(rename = "maxStreak")]
- pub max_streak: i32,
+ pub max_streak: Option,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct MemberStatus {
+ #[serde(rename = "onDate")]
+ pub on_date: Option,
+ pub streak: Option,
+ #[serde(rename = "consecutiveMisses")]
+ pub consecutive_misses: Option,
}
#[derive(Clone, Debug, Deserialize)]
@@ -42,11 +49,9 @@ pub struct Member {
pub name: String,
#[serde(rename = "discordId")]
pub discord_id: String,
- #[serde(rename = "groupId")]
- pub group_id: i32,
- #[serde(default)]
- pub streak: Vec, // Note that Root will NOT have multiple Streak elements but it may be an empty list which is why we use a vector here
- pub track: Option
+ pub track: Option,
+ pub year: i32,
+ pub status: Option,
}
#[derive(Debug, Deserialize, Clone)]
diff --git a/src/graphql/queries.rs b/src/graphql/queries.rs
index e69d078..0cb6a6f 100644
--- a/src/graphql/queries.rs
+++ b/src/graphql/queries.rs
@@ -16,299 +16,131 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
use anyhow::{anyhow, Context};
-use chrono::Local;
+use chrono::{Local, NaiveDate};
use serde_json::Value;
use tracing::debug;
-use crate::graphql::models::{AttendanceRecord, Member, Streak};
+use crate::graphql::models::{AttendanceRecord, Member};
-use super::models::StreakWithMemberId;
+use super::GraphQLClient;
-pub async fn fetch_members() -> anyhow::Result> {
- let request_url = std::env::var("ROOT_URL").context("ROOT_URL not found in ENV")?;
-
- let client = reqwest::Client::new();
- let query = r#"
- {
- members {
+impl GraphQLClient {
+ pub async fn fetch_member_data(&self, date: NaiveDate) -> anyhow::Result> {
+ let query = r#"
+ query($date: NaiveDate!) {
+ allMembers {
memberId
name
discordId
groupId
- streak {
- currentStreak
- maxStreak
+ status {
+ onDate(date: $date) {
+ isSent
+ onBreak
+ }
+ streak {
+ currentStreak,
+ maxStreak
+ }
+ consecutiveMisses
}
track
- }
- }"#;
-
- debug!("Sending query {}", query);
- let response = client
- .post(request_url)
- .json(&serde_json::json!({"query": query}))
- .send()
- .await
- .context("Failed to successfully post request")?;
-
- if !response.status().is_success() {
- return Err(anyhow!(
- "Server responded with an error: {:?}",
- response.status()
- ));
- }
-
- let response_json: serde_json::Value = response
- .json()
- .await
- .context("Failed to serialize response")?;
-
- debug!("Response: {}", response_json);
- let members = response_json
- .get("data")
- .and_then(|data| data.get("members"))
- .and_then(|members| members.as_array())
- .ok_or_else(|| {
- anyhow::anyhow!(
- "Malformed response: Could not access Members from {}",
- response_json
- )
- })?;
-
- let members: Vec = serde_json::from_value(serde_json::Value::Array(members.clone()))
- .context("Failed to parse 'members' into Vec")?;
+ year
+ }
+ }"#;
- Ok(members)
-}
+ debug!("Sending query {}", query);
-pub async fn increment_streak(member: &mut Member) -> anyhow::Result<()> {
- let request_url = std::env::var("ROOT_URL").context("ROOT_URL was not found in ENV")?;
+ let variables = serde_json::json!({
+ "date": date.format("%Y-%m-%d").to_string()
+ });
- let client = reqwest::Client::new();
- let mutation = format!(
- r#"
- mutation {{
- incrementStreak(input: {{ memberId: {} }}) {{
- currentStreak
- maxStreak
- }}
- }}"#,
- member.member_id
- );
+ debug!("With variables: {}", variables);
- debug!("Sending mutation {}", mutation);
- let response = client
- .post(request_url)
- .json(&serde_json::json!({"query": mutation}))
- .send()
- .await
- .context("Failed to succesfully post query to Root")?;
+ let response = self
+ .http()
+ .post(self.root_url())
+ .json(&serde_json::json!({"query": query, "variables":variables}))
+ .send()
+ .await
+ .context("Failed to successfully post request")?;
- if !response.status().is_success() {
- return Err(anyhow!(
- "Server responded with an error: {:?}",
- response.status()
- ));
- }
- let response_json: serde_json::Value = response
- .json()
- .await
- .context("Failed to parse response JSON")?;
- debug!("Response: {}", response_json);
-
- if let Some(data) = response_json
- .get("data")
- .and_then(|data| data.get("incrementStreak"))
- {
- let current_streak =
- data.get("currentStreak")
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow!("current_streak was parsed as None"))? as i32;
- let max_streak =
- data.get("maxStreak")
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow!("max_streak was parsed as None"))? as i32;
-
- if member.streak.is_empty() {
- member.streak.push(Streak {
- current_streak,
- max_streak,
- });
- } else {
- for streak in &mut member.streak {
- streak.current_streak = current_streak;
- streak.max_streak = max_streak;
- }
+ if !response.status().is_success() {
+ return Err(anyhow!(
+ "Server responded with an error: {:?}",
+ response.status()
+ ));
}
- } else {
- return Err(anyhow!(
- "Failed to access data from response: {}",
- response_json
- ));
- }
-
- Ok(())
-}
-
-pub async fn reset_streak(member: &mut Member) -> anyhow::Result<()> {
- let request_url = std::env::var("ROOT_URL").context("ROOT_URL was not found in the ENV")?;
-
- let client = reqwest::Client::new();
- let mutation = format!(
- r#"
- mutation {{
- resetStreak(input: {{ memberId: {} }}) {{
- currentStreak
- maxStreak
- }}
- }}"#,
- member.member_id
- );
-
- debug!("Sending mutation {}", mutation);
- let response = client
- .post(&request_url)
- .json(&serde_json::json!({ "query": mutation }))
- .send()
- .await
- .context("Failed to succesfully post query to Root")?;
- if !response.status().is_success() {
- return Err(anyhow!(
- "Server responded with an error: {:?}",
- response.status()
- ));
+ let response_json: serde_json::Value = response
+ .json()
+ .await
+ .context("Failed to serialize response")?;
+
+ debug!("Response: {}", response_json);
+ let members = response_json
+ .get("data")
+ .and_then(|data| data.get("allMembers"))
+ .and_then(|members| members.as_array())
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "Malformed response: Could not access Members from {}",
+ response_json
+ )
+ })?;
+
+ let members: Vec =
+ serde_json::from_value(serde_json::Value::Array(members.clone()))
+ .context("Failed to parse 'members' into Vec")?;
+
+ Ok(members)
}
- let response_json: serde_json::Value = response
- .json()
- .await
- .context("Failed to parse response JSON")?;
- debug!("Response: {}", response_json);
+ pub async fn fetch_attendance(&self) -> anyhow::Result> {
+ debug!("Fetching attendance data");
- if let Some(data) = response_json
- .get("data")
- .and_then(|data| data.get("resetStreak"))
- {
- let current_streak =
- data.get("currentStreak")
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow!("current_streak was parsed as None"))? as i32;
- let max_streak =
- data.get("maxStreak")
- .and_then(|v| v.as_i64())
- .ok_or_else(|| anyhow!("max_streak was parsed as None"))? as i32;
-
- if member.streak.is_empty() {
- member.streak.push(Streak {
- current_streak,
- max_streak,
- });
- } else {
- for streak in &mut member.streak {
- streak.current_streak = current_streak;
- streak.max_streak = max_streak;
- }
- }
- } else {
- return Err(anyhow!("Failed to access data from {}", response_json));
- }
-
- Ok(())
-}
-
-pub async fn fetch_attendance() -> anyhow::Result> {
- let request_url =
- std::env::var("ROOT_URL").context("ROOT_URL environment variable not found")?;
-
- debug!("Fetching attendance data from {}", request_url);
-
- let client = reqwest::Client::new();
- let today = Local::now().format("%Y-%m-%d").to_string();
- let query = format!(
- r#"
+ let today = Local::now().format("%Y-%m-%d").to_string();
+ let query = format!(
+ r#"
query {{
- attendanceByDate(date: "{}") {{
+ attendanceByDate(date: "{today}") {{
name,
year,
isPresent,
timeIn,
}}
- }}"#,
- today
- );
-
- let response = client
- .post(&request_url)
- .json(&serde_json::json!({ "query": query }))
- .send()
- .await
- .context("Failed to send GraphQL request")?;
- debug!("Response status: {:?}", response.status());
-
- let json: Value = response
- .json()
- .await
- .context("Failed to parse response as JSON")?;
-
- let attendance_array = json["data"]["attendanceByDate"]
- .as_array()
- .context("Missing or invalid 'data.attendanceByDate' array in response")?;
-
- let attendance: Vec = attendance_array
- .iter()
- .map(|entry| {
- serde_json::from_value(entry.clone()).context("Failed to parse attendance record")
- })
- .collect::>>()?;
-
- debug!(
- "Successfully fetched {} attendance records",
- attendance.len()
- );
- Ok(attendance)
-}
-
-pub async fn fetch_streaks() -> anyhow::Result> {
- let request_url = std::env::var("ROOT_URL").context("ROOT_URL not found in ENV")?;
-
- let client = reqwest::Client::new();
- let query = r#"
- {
- streaks {
- memberId
- currentStreak
- maxStreak
- }
- }
- "#;
-
- debug!("Sending query {}", query);
- let response = client
- .post(request_url)
- .json(&serde_json::json!({"query": query}))
- .send()
- .await
- .context("Failed to successfully post request")?;
-
- if !response.status().is_success() {
- return Err(anyhow!(
- "Server responded with an error: {:?}",
- response.status()
- ));
+ }}"#
+ );
+
+ let response = self
+ .http()
+ .post(self.root_url())
+ .json(&serde_json::json!({ "query": query }))
+ .send()
+ .await
+ .context("Failed to send GraphQL request")?;
+ debug!("Response status: {:?}", response.status());
+
+ let json: Value = response
+ .json()
+ .await
+ .context("Failed to parse response as JSON")?;
+
+ let attendance_array = json["data"]["attendanceByDate"]
+ .as_array()
+ .context("Missing or invalid 'data.attendanceByDate' array in response")?;
+
+ let attendance: Vec = attendance_array
+ .iter()
+ .map(|entry| {
+ serde_json::from_value(entry.clone()).context("Failed to parse attendance record")
+ })
+ .collect::>>()?;
+
+ debug!(
+ "Successfully fetched {} attendance records",
+ attendance.len()
+ );
+ Ok(attendance)
}
-
- let response_json: serde_json::Value = response
- .json()
- .await
- .context("Failed to serialize response")?;
-
- debug!("Response: {}", response_json);
- let streaks = response_json
- .get("data")
- .and_then(|data| data.get("streaks"))
- .and_then(|streaks| serde_json::from_value::>(streaks.clone()).ok())
- .context("Failed to parse streaks data")?;
-
- Ok(streaks)
}
diff --git a/src/ids.rs b/src/ids.rs
index 3020a96..fbf20d3 100644
--- a/src/ids.rs
+++ b/src/ids.rs
@@ -18,6 +18,12 @@ along with this program. If not, see .
/// Points to the Embed in the #roles channel.
pub const ROLES_MESSAGE_ID: u64 = 1298636092886749294;
+/// Fourth and Third Year Roles for privileged commands
+#[allow(dead_code)]
+pub const FOURTH_YEAR_ROLE_ID: u64 = 1135793659040772240;
+#[allow(dead_code)]
+pub const THIRD_YEAR_ROLE_ID: u64 = 1166292683317321738;
+
// Role IDs
pub const ARCHIVE_ROLE_ID: u64 = 1208457364274028574;
pub const MOBILE_ROLE_ID: u64 = 1298553701094395936;
@@ -27,10 +33,5 @@ pub const RESEARCH_ROLE_ID: u64 = 1298553855474270219;
pub const DEVOPS_ROLE_ID: u64 = 1298553883169132554;
pub const WEB_ROLE_ID: u64 = 1298553910167994428;
-// Channel IDs for status updates
-pub const SYSTEMS_CHANNEL_ID: u64 = 1378426650152271902;
-pub const MOBILE_CHANNEL_ID: u64 = 1378685538835365960;
-pub const WEB_CHANNEL_ID: u64 = 1378685360133115944;
-pub const AI_CHANNEL_ID: u64 = 1343489220068507649;
pub const STATUS_UPDATE_CHANNEL_ID: u64 = 764575524127244318;
pub const THE_LAB_CHANNEL_ID: u64 = 1208438766893670451;
diff --git a/src/main.rs b/src/main.rs
index e1faf4c..5137f7a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16,146 +16,130 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
mod commands;
+mod config;
mod graphql;
mod ids;
mod reaction_roles;
-/// This module is a simple cron equivalent. It spawns threads for the [`Task`]s that need to be completed.
mod scheduler;
-/// A trait to define a job that needs to be executed regularly, for example checking for status updates daily.
mod tasks;
+mod trace;
mod utils;
use anyhow::Context as _;
+use config::Config;
+use graphql::GraphQLClient;
use poise::{Context as PoiseContext, Framework, FrameworkOptions, PrefixFrameworkOptions};
-use reaction_roles::{handle_reaction, populate_data_with_reaction_roles};
+use reaction_roles::handle_reaction;
+use serenity::client::ClientBuilder;
+use serenity::Client;
use serenity::{
all::{ReactionType, RoleId, UserId},
client::{Context as SerenityContext, FullEvent},
model::gateway::GatewayIntents,
};
-use tokio::sync::RwLock;
-use tracing::info;
-use tracing_subscriber::{fmt, layer::SubscriberExt, reload, EnvFilter, Registry};
-
-use std::{
- collections::{HashMap, HashSet},
- fs::File,
- sync::Arc,
-};
+use trace::{setup_tracing, ReloadHandle};
+use tracing::{debug, info, instrument};
+
+use std::collections::HashMap;
-pub type Error = Box;
-pub type Context<'a> = PoiseContext<'a, Data, Error>;
-pub type ReloadHandle = Arc>>;
+type Error = Box;
+type Context<'a> = PoiseContext<'a, Data, Error>;
-pub struct Data {
- pub reaction_roles: HashMap,
- pub log_reload_handle: ReloadHandle,
+/// The [`Data`] struct is kept in-memory by the Bot till it shutsdown and can be used to store session-persistent data.
+#[derive(Clone)]
+struct Data {
+ reaction_roles: HashMap,
+ log_reload_handle: ReloadHandle,
+ graphql_client: GraphQLClient,
}
-fn setup_tracing() -> anyhow::Result {
- let env = std::env::var("AMD_RUST_ENV").context("RUST_ENV was not found in the ENV")?;
- let enable_debug_libraries_string = std::env::var("ENABLE_DEBUG_LIBRARIES")
- .context("ENABLE_DEBUG_LIBRARIES was not found in the ENV")?;
- let enable_debug_libraries: bool = enable_debug_libraries_string
- .parse()
- .context("Failed to parse ENABLE_DEBUG_LIBRARIES")?;
- let crate_name = env!("CARGO_CRATE_NAME");
-
- let (filter, reload_handle) = reload::Layer::new(EnvFilter::new(
- if env == "production" && enable_debug_libraries {
- "info".to_string()
- } else if env == "production" && !enable_debug_libraries {
- format!("{crate_name}=info")
- } else if enable_debug_libraries {
- "trace".to_string()
- } else {
- format!("{crate_name}=trace")
- },
- ));
-
- if env != "production" {
- let subscriber = tracing_subscriber::registry()
- .with(filter)
- .with(fmt::layer().pretty().with_writer(std::io::stdout))
- .with(
- fmt::layer()
- .pretty()
- .with_ansi(false)
- .with_writer(File::create("amd.log").context("Failed to create subscriber")?),
- );
-
- tracing::subscriber::set_global_default(subscriber).context("Failed to set subscriber")?;
- Ok(Arc::new(RwLock::new(reload_handle)))
- } else {
- let subscriber = tracing_subscriber::registry().with(filter).with(
- fmt::layer()
- .pretty()
- .with_ansi(false)
- .with_writer(File::create("amd.log").context("Failed to create subscriber")?),
- );
-
- tracing::subscriber::set_global_default(subscriber).context("Failed to set subscriber")?;
- Ok(Arc::new(RwLock::new(reload_handle)))
+impl Data {
+ /// Returns a new [`Data`] with an empty `reaction_roles` field and the passed-in `reload_handle`.
+ fn new(reload_handle: ReloadHandle, root_url: String) -> Self {
+ Data {
+ reaction_roles: HashMap::new(),
+ log_reload_handle: reload_handle,
+ graphql_client: GraphQLClient::new(root_url),
+ }
}
}
-#[tokio::main]
-async fn main() -> Result<(), Error> {
- dotenv::dotenv().ok();
- let reload_handle = setup_tracing().context("Failed to setup tracing")?;
-
- info!("Tracing initialized. Continuing main...");
- let mut data = Data {
- reaction_roles: HashMap::new(),
- log_reload_handle: reload_handle,
- };
- populate_data_with_reaction_roles(&mut data);
-
- let discord_token =
- std::env::var("DISCORD_TOKEN").context("DISCORD_TOKEN was not found in the ENV")?;
- let owner_id: u64 = std::env::var("OWNER_ID")
- .context("OWNER_ID was not found in the ENV")?
- .parse()
- .context("Failed to parse owner_id")?;
- let owner_user_id = UserId::from(owner_id);
-
- let framework = Framework::builder()
+/// Builds a [`poise::Framework`] with the given arguments and commands from [`commands::get_commands`].
+#[instrument(level = "debug", skip(data))]
+fn build_framework(
+ owners: Option,
+ prefix_string: String,
+ data: Data,
+) -> Framework {
+ Framework::builder()
.options(FrameworkOptions {
commands: commands::get_commands(),
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
prefix_options: PrefixFrameworkOptions {
- prefix: Some(String::from("$")),
+ prefix: Some(prefix_string),
..Default::default()
},
- owners: HashSet::from([owner_user_id]),
+ owners: owners.into_iter().collect(),
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
- scheduler::run_scheduler(ctx.clone()).await;
+ scheduler::run_scheduler(ctx.clone(), data.graphql_client.clone()).await;
Ok(data)
})
})
- .build();
+ .build()
+}
+
+fn prepare_data(config: &Config, reload_handle: ReloadHandle) -> Data {
+ let mut data = Data::new(reload_handle, config.root_url.clone());
+ data.populate_with_reaction_roles();
+ data
+}
- let mut client = serenity::client::ClientBuilder::new(
- discord_token,
+async fn build_client(config: &Config, data: Data) -> Result {
+ ClientBuilder::new(
+ config.discord_token.clone(),
GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT,
)
- .framework(framework)
+ .framework(build_framework(
+ config.owner_id,
+ config.prefix_string.clone(),
+ data,
+ ))
.await
- .context("Failed to create the Serenity client")?;
+ .context("Failed to create the Serenity client")
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Error> {
+ dotenv::dotenv().ok();
+ let config = Config::default();
+
+ let reload_handle = setup_tracing(config.debug, config.enable_debug_libraries)
+ .context("Failed to setup tracing")?;
+
+ info!(
+ "Starting {} v{}",
+ env!("CARGO_PKG_NAME"),
+ env!("CARGO_PKG_VERSION")
+ );
+ debug!(
+ "Configuration loaded: debug={}, enable_debug_libraries={}, owner_id={:?}, prefix_string={}, root_url={}",
+ config.debug, config.enable_debug_libraries, config.owner_id, config.prefix_string, config.root_url
+ );
+
+ let data = prepare_data(&config, reload_handle);
+ let mut client = build_client(&config, data).await?;
client
.start()
.await
.context("Failed to start the Serenity client")?;
- info!("Starting amD...");
-
Ok(())
}
@@ -167,10 +151,10 @@ async fn event_handler(
) -> Result<(), Error> {
match event {
FullEvent::ReactionAdd { add_reaction } => {
- handle_reaction(ctx, add_reaction, data, true).await;
+ handle_reaction(ctx, add_reaction, data, true).await?;
}
FullEvent::ReactionRemove { removed_reaction } => {
- handle_reaction(ctx, removed_reaction, data, false).await;
+ handle_reaction(ctx, removed_reaction, data, false).await?;
}
_ => {}
}
diff --git a/src/reaction_roles.rs b/src/reaction_roles.rs
index 40e603b..5491795 100644
--- a/src/reaction_roles.rs
+++ b/src/reaction_roles.rs
@@ -1,50 +1,52 @@
use std::collections::HashMap;
use serenity::all::{Context as SerenityContext, MessageId, Reaction, ReactionType, RoleId};
-use tracing::{debug, error};
+use tracing::debug;
use crate::{
ids::{
AI_ROLE_ID, ARCHIVE_ROLE_ID, DEVOPS_ROLE_ID, MOBILE_ROLE_ID, RESEARCH_ROLE_ID,
ROLES_MESSAGE_ID, SYSTEMS_ROLE_ID, WEB_ROLE_ID,
},
- Data,
+ Data, Error,
};
-pub fn populate_data_with_reaction_roles(data: &mut Data) {
- let roles = [
- (
- ReactionType::Unicode("📁".to_string()),
- RoleId::new(ARCHIVE_ROLE_ID),
- ),
- (
- ReactionType::Unicode("📱".to_string()),
- RoleId::new(MOBILE_ROLE_ID),
- ),
- (
- ReactionType::Unicode("⚙️".to_string()),
- RoleId::new(SYSTEMS_ROLE_ID),
- ),
- (
- ReactionType::Unicode("🤖".to_string()),
- RoleId::new(AI_ROLE_ID),
- ),
- (
- ReactionType::Unicode("📜".to_string()),
- RoleId::new(RESEARCH_ROLE_ID),
- ),
- (
- ReactionType::Unicode("🚀".to_string()),
- RoleId::new(DEVOPS_ROLE_ID),
- ),
- (
- ReactionType::Unicode("🌐".to_string()),
- RoleId::new(WEB_ROLE_ID),
- ),
- ];
+impl Data {
+ pub fn populate_with_reaction_roles(&mut self) {
+ let roles = [
+ (
+ ReactionType::Unicode("📁".to_string()),
+ RoleId::new(ARCHIVE_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("📱".to_string()),
+ RoleId::new(MOBILE_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("⚙️".to_string()),
+ RoleId::new(SYSTEMS_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("🤖".to_string()),
+ RoleId::new(AI_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("📜".to_string()),
+ RoleId::new(RESEARCH_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("🚀".to_string()),
+ RoleId::new(DEVOPS_ROLE_ID),
+ ),
+ (
+ ReactionType::Unicode("🌐".to_string()),
+ RoleId::new(WEB_ROLE_ID),
+ ),
+ ];
- data.reaction_roles
- .extend::>(roles.into());
+ self.reaction_roles
+ .extend::>(roles.into());
+ }
}
pub async fn handle_reaction(
@@ -52,39 +54,28 @@ pub async fn handle_reaction(
reaction: &Reaction,
data: &Data,
is_add: bool,
-) {
+) -> Result<(), Error> {
if !is_relevant_reaction(reaction.message_id, &reaction.emoji, data) {
- return;
+ return Ok(());
}
debug!("Handling {:?} from {:?}.", reaction.emoji, reaction.user_id);
- // TODO Log these errors
- let Some(guild_id) = reaction.guild_id else {
- return;
- };
- let Some(user_id) = reaction.user_id else {
- return;
- };
- let Ok(member) = guild_id.member(ctx, user_id).await else {
- return;
- };
- let Some(role_id) = data.reaction_roles.get(&reaction.emoji) else {
- return;
- };
+ let guild_id = reaction.guild_id.ok_or("No guild_id")?;
+ let user_id = reaction.user_id.ok_or("No user_id")?;
+ let member = guild_id.member(ctx, user_id).await?;
+ let role_id = data
+ .reaction_roles
+ .get(&reaction.emoji)
+ .ok_or("No role mapping")?;
- let result = if is_add {
- member.add_role(&ctx.http, *role_id).await
+ if is_add {
+ member.add_role(&ctx.http, *role_id).await?;
} else {
- member.remove_role(&ctx.http, *role_id).await
- };
-
- if let Err(e) = result {
- error!(
- "Could not handle {:?} from {:?}. Error: {}",
- reaction.emoji, reaction.user_id, e
- );
+ member.remove_role(&ctx.http, *role_id).await?;
}
+
+ Ok(())
}
fn is_relevant_reaction(message_id: MessageId, emoji: &ReactionType, data: &Data) -> bool {
diff --git a/src/scheduler.rs b/src/scheduler.rs
index 83aedc5..3d61a1c 100644
--- a/src/scheduler.rs
+++ b/src/scheduler.rs
@@ -15,30 +15,34 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-use crate::tasks::{get_tasks, Task};
+//! This module is a simple cron equivalent. It spawns threads for the [`Task`]s that need to be completed.
+use crate::{
+ graphql::GraphQLClient,
+ tasks::{get_tasks, Task},
+};
use serenity::client::Context as SerenityContext;
use tokio::spawn;
-use tracing::{debug, error, trace};
+use tracing::{debug, error, instrument};
-pub async fn run_scheduler(ctx: SerenityContext) {
- trace!("Running scheduler");
+#[instrument(level = "debug", skip(ctx))]
+pub async fn run_scheduler(ctx: SerenityContext, client: GraphQLClient) {
let tasks = get_tasks();
for task in tasks {
- debug!("Spawing task {}", task.name());
- spawn(schedule_task(ctx.clone(), task));
+ // TODO: Panics in this thread might be silent and won't be noticed. It should be caught, safely unwinded and ideally reported.
+ spawn(schedule_task(ctx.clone(), task, client.clone()));
}
}
-async fn schedule_task(ctx: SerenityContext, task: Box) {
+#[instrument(level = "debug", skip(ctx))]
+async fn schedule_task(ctx: SerenityContext, task: Box, client: GraphQLClient) {
loop {
let next_run_in = task.run_in();
- debug!("Task {}: Next run in {:?}", task.name(), next_run_in);
tokio::time::sleep(next_run_in).await;
debug!("Running task {}", task.name());
- if let Err(e) = task.run(ctx.clone()).await {
+ if let Err(e) = task.run(ctx.clone(), client.clone()).await {
error!("Could not run task {}, error {}", task.name(), e);
}
}
diff --git a/src/tasks/lab_attendance.rs b/src/tasks/lab_attendance.rs
index 77afd75..e34e90b 100644
--- a/src/tasks/lab_attendance.rs
+++ b/src/tasks/lab_attendance.rs
@@ -25,8 +25,9 @@ use serenity::async_trait;
use std::collections::HashMap;
use tracing::{debug, trace};
+use crate::graphql::GraphQLClient;
use crate::{
- graphql::{models::AttendanceRecord, queries::fetch_attendance},
+ graphql::models::AttendanceRecord,
ids::THE_LAB_CHANNEL_ID,
utils::time::{get_five_forty_five_pm_timestamp, time_until},
};
@@ -46,14 +47,18 @@ impl Task for PresenseReport {
time_until(18, 00)
}
- async fn run(&self, ctx: SerenityContext) -> anyhow::Result<()> {
- check_lab_attendance(ctx).await
+ async fn run(&self, ctx: SerenityContext, client: GraphQLClient) -> anyhow::Result<()> {
+ check_lab_attendance(ctx, client).await
}
}
-pub async fn check_lab_attendance(ctx: SerenityContext) -> anyhow::Result<()> {
+pub async fn check_lab_attendance(
+ ctx: SerenityContext,
+ client: GraphQLClient,
+) -> anyhow::Result<()> {
trace!("Starting lab attendance check");
- let attendance = fetch_attendance()
+ let attendance = client
+ .fetch_attendance()
.await
.context("Failed to fetch attendance from Root")?;
@@ -97,7 +102,7 @@ async fn send_lab_closed_message(ctx: SerenityContext) -> anyhow::Result<()> {
.unwrap_or_else(|| bot_user.default_avatar_url());
let embed = CreateEmbed::new()
- .title(format!("Presense Report - {}", today_date))
+ .title(format!("Presense Report - {today_date}"))
.url(TITLE_URL)
.author(
CreateEmbedAuthor::new("amD")
@@ -156,7 +161,7 @@ async fn send_attendance_report(
description.push_str(&format_attendance_list("Late", &late_list));
let embed = CreateEmbed::new()
- .title(format!("Presense Report - {}", today_date))
+ .title(format!("Presense Report - {today_date}"))
.url(TITLE_URL)
.author(
CreateEmbedAuthor::new("amD")
@@ -191,15 +196,15 @@ fn format_attendance_list(title: &str, list: &[AttendanceRecord]) -> String {
}
}
- let mut result = format!("# {}\n", title);
+ let mut result = format!("# {title}\n");
for year in 1..=3 {
if let Some(names) = by_year.get(&year) {
if !names.is_empty() {
- result.push_str(&format!("### Year {}\n", year));
+ result.push_str(&format!("### Year {year}\n"));
for name in names {
- result.push_str(&format!("- {}\n", name));
+ result.push_str(&format!("- {name}\n"));
}
result.push('\n');
}
diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs
index b856125..f4f1b8b 100644
--- a/src/tasks/mod.rs
+++ b/src/tasks/mod.rs
@@ -15,16 +15,21 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
+//! A trait to define a job that needs to be executed regularly, for example checking for status updates daily.
mod lab_attendance;
mod status_update;
+use std::fmt::{self, Debug};
+
use anyhow::Result;
use async_trait::async_trait;
use lab_attendance::PresenseReport;
use serenity::client::Context;
-use status_update::StatusUpdateCheck;
+use status_update::StatusUpdateReport;
use tokio::time::Duration;
+use crate::graphql::GraphQLClient;
+
/// A [`Task`] is any job that needs to be executed on a regular basis.
/// A task has a function [`Task::run_in`] that returns the time till the
/// next ['Task::run`] is run.
@@ -32,11 +37,20 @@ use tokio::time::Duration;
pub trait Task: Send + Sync {
fn name(&self) -> &str;
fn run_in(&self) -> Duration;
- async fn run(&self, ctx: Context) -> Result<()>;
+ async fn run(&self, ctx: Context, client: GraphQLClient) -> Result<()>;
+}
+
+impl Debug for Box {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Task")
+ .field("name", &self.name())
+ .field("run in", &self.run_in())
+ .finish()
+ }
}
/// Analogous to [`crate::commands::get_commands`], every task that is defined
/// must be included in the returned vector in order for it to be scheduled.
pub fn get_tasks() -> Vec> {
- vec![Box::new(StatusUpdateCheck), Box::new(PresenseReport)]
+ vec![Box::new(PresenseReport), Box::new(StatusUpdateReport)]
}
diff --git a/src/tasks/status_update.rs b/src/tasks/status_update.rs
index 3711b3b..23e9128 100644
--- a/src/tasks/status_update.rs
+++ b/src/tasks/status_update.rs
@@ -15,58 +15,49 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-use std::collections::{HashMap, HashSet};
+use std::collections::HashMap;
-use chrono::{DateTime, Utc};
-use serenity::all::{
- CacheHttp, ChannelId, Context, CreateEmbed, CreateMessage, GetMessages, Message,
-};
+use serenity::all::{CacheHttp, ChannelId, Context, CreateEmbed, CreateMessage};
use serenity::async_trait;
+use tracing::instrument;
use super::Task;
-use crate::graphql::models::{Member, StreakWithMemberId};
-use crate::graphql::queries::{fetch_members, fetch_streaks, increment_streak, reset_streak};
-use crate::ids::{
- AI_CHANNEL_ID, MOBILE_CHANNEL_ID, STATUS_UPDATE_CHANNEL_ID, SYSTEMS_CHANNEL_ID, WEB_CHANNEL_ID,
-};
+use crate::graphql::models::Member;
+use crate::graphql::GraphQLClient;
+use crate::ids::STATUS_UPDATE_CHANNEL_ID;
use crate::utils::time::time_until;
/// Checks for status updates daily at 5 AM.
-pub struct StatusUpdateCheck;
+pub struct StatusUpdateReport;
#[async_trait]
-impl Task for StatusUpdateCheck {
+impl Task for StatusUpdateReport {
fn name(&self) -> &str {
- "Status Update Check"
+ "Status Update Report"
}
fn run_in(&self) -> tokio::time::Duration {
- time_until(5, 00)
+ time_until(5, 15)
+ // Duration::from_secs(1) // for development
}
- async fn run(&self, ctx: Context) -> anyhow::Result<()> {
- status_update_check(ctx).await
+ async fn run(&self, ctx: Context, client: GraphQLClient) -> anyhow::Result<()> {
+ status_update_check(ctx, client).await
}
}
type GroupedMember = HashMap