aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore5
-rw-r--r--Cargo.lock728
-rw-r--r--Cargo.toml3
-rw-r--r--build.rs5
-rw-r--r--flake.nix2
-rw-r--r--migrations/20240116060938_init_x500s.sql27
-rw-r--r--src/bin/add-student-x500s.rs144
-rw-r--r--src/bin/lambo.rs (renamed from src/main.rs)60
-rw-r--r--src/config.rs17
-rw-r--r--src/handlers/x500_mapper.rs21
-rw-r--r--src/lib.rs2
12 files changed, 975 insertions, 40 deletions
diff --git a/.envrc b/.envrc
index 3550a30..9ca2500 100644
--- a/.envrc
+++ b/.envrc
@@ -1 +1,2 @@
use flake
+export DATABASE_URL=sqlite:lambo.db
diff --git a/.gitignore b/.gitignore
index 8283a43..b9367b3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,5 +12,8 @@ result-*
# Non-Nix outputs
target/
-# Configuration files
+# Configuration and state files
+lambo.db
+lambo.db-shm
+lambo.db-wal
lambo.toml
diff --git a/Cargo.lock b/Cargo.lock
index 740a581..32a0ffc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -18,6 +18,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
+name = "ahash"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
+dependencies = [
+ "cfg-if",
+ "getrandom",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
+[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -110,6 +129,25 @@ dependencies = [
]
[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-write-file"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
+dependencies = [
+ "nix",
+ "rand",
+]
+
+[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -148,6 +186,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -158,6 +202,9 @@ name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+dependencies = [
+ "serde",
+]
[[package]]
name = "block-buffer"
@@ -311,6 +358,12 @@ dependencies = [
]
[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -336,6 +389,21 @@ dependencies = [
]
[[package]]
+name = "crc"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -354,6 +422,15 @@ dependencies = [
]
[[package]]
+name = "crossbeam-queue"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -370,6 +447,27 @@ dependencies = [
]
[[package]]
+name = "csv"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -390,6 +488,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
+name = "der"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -406,7 +515,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
+ "const-oid",
"crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+dependencies = [
+ "serde",
]
[[package]]
@@ -444,12 +570,35 @@ dependencies = [
]
[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
+name = "finl_unicode"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
+
+[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -460,6 +609,17 @@ dependencies = [
]
[[package]]
+name = "flume"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin 0.9.8",
+]
+
+[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -517,6 +677,17 @@ dependencies = [
]
[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -629,12 +800,28 @@ name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
+dependencies = [
+ "hashbrown",
+]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+dependencies = [
+ "unicode-segmentation",
+]
[[package]]
name = "hermit-abi"
@@ -652,6 +839,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "http"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -773,6 +993,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
+name = "itertools"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
+dependencies = [
+ "either",
+]
+
+[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -793,16 +1022,28 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
+ "csv",
"futures",
"log",
"serde",
"serenity",
+ "sqlx",
"stderrlog",
+ "time",
"tokio",
"toml",
]
[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+dependencies = [
+ "spin 0.5.2",
+]
+
+[[package]]
name = "levenshtein"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -815,6 +1056,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
name = "linux-raw-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -837,6 +1095,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
name = "memchr"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -874,6 +1142,12 @@ dependencies = [
]
[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -894,12 +1168,72 @@ dependencies = [
]
[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags 2.4.1",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
name = "num-traits"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
dependencies = [
"autocfg",
+ "libm",
]
[[package]]
@@ -951,6 +1285,21 @@ dependencies = [
]
[[package]]
+name = "paste"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -969,6 +1318,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1100,12 +1476,32 @@ dependencies = [
"cc",
"getrandom",
"libc",
- "spin",
+ "spin 0.9.8",
"untrusted",
"windows-sys 0.48.0",
]
[[package]]
+name = "rsa"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
name = "rustc-demangle"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1305,6 +1701,27 @@ dependencies = [
]
[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
name = "skeptic"
version = "0.13.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1346,9 +1763,240 @@ dependencies = [
[[package]]
name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c"
+dependencies = [
+ "itertools",
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
+dependencies = [
+ "ahash",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "crc",
+ "crossbeam-queue",
+ "dotenvy",
+ "either",
+ "event-listener",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
+dependencies = [
+ "atomic-write-file",
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 1.0.109",
+ "tempfile",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.4.1",
+ "byteorder",
+ "bytes",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "time",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.4.1",
+ "byteorder",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "time",
+ "tracing",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
+dependencies = [
+ "atoi",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "sqlx-core",
+ "time",
+ "tracing",
+ "url",
+ "urlencoding",
+]
[[package]]
name = "static_assertions"
@@ -1370,12 +2018,29 @@ dependencies = [
]
[[package]]
+name = "stringprep"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6"
+dependencies = [
+ "finl_unicode",
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
+name = "subtle"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1559,6 +2224,17 @@ dependencies = [
]
[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "tokio-tungstenite"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1763,6 +2439,18 @@ dependencies = [
]
[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1781,6 +2469,12 @@ dependencies = [
]
[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1799,6 +2493,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0"
[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1925,6 +2625,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
[[package]]
+name = "whoami"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50"
+
+[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2116,6 +2822,26 @@ dependencies = [
]
[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.48",
+]
+
+[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index dd2c38e..767144c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,10 +6,13 @@ edition = "2021"
[dependencies]
anyhow = { version = "1.0.79", features = ["backtrace"] }
clap = { version = "4.4.17", features = ["derive"] }
+csv = "1.3.0"
futures = "0.3.30"
log = "0.4.20"
serde = { version = "1.0.195", features = ["derive"] }
serenity = "0.12.0"
+sqlx = { version = "0.7.3", features = ["runtime-tokio", "sqlite", "time"] }
stderrlog = "0.5.4"
+time = "0.3.31"
tokio = { version = "1.35.1", features = ["rt-multi-thread"] }
toml = "0.8.8"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..d506869
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,5 @@
+// generated by `sqlx migrate build-script`
+fn main() {
+ // trigger recompilation when a new migration is added
+ println!("cargo:rerun-if-changed=migrations");
+}
diff --git a/flake.nix b/flake.nix
index d55351f..15c47ca 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,7 +20,7 @@
in rec {
devShells.default = pkgs.mkShell {
inputsFrom = builtins.attrValues packages;
- nativeBuildInputs = [ pkgs.cargo-watch ];
+ nativeBuildInputs = [ pkgs.cargo-watch pkgs.sqlite pkgs.sqlx-cli ];
};
packages.default =
diff --git a/migrations/20240116060938_init_x500s.sql b/migrations/20240116060938_init_x500s.sql
new file mode 100644
index 0000000..3d5ca68
--- /dev/null
+++ b/migrations/20240116060938_init_x500s.sql
@@ -0,0 +1,27 @@
+-- We know all the X.500s of students we expect to have.
+CREATE TABLE IF NOT EXISTS student_x500s
+ ( x500 TEXT NOT NULL
+ , UNIQUE(x500)
+ );
+
+-- In the common case, we can just map users to their X.500s.
+CREATE TABLE IF NOT EXISTS uids_to_x500s_clean
+ ( uid TEXT NOT NULL
+ , x500 TEXT NOT NULL
+ , FOREIGN KEY(x500) REFERENCES student_x500s(x500)
+ , UNIQUE(uid)
+ , UNIQUE(x500)
+ );
+
+-- ...until we see someone change X.500, or try to take someone else's X.500,
+-- or use an unenrolled student's X.500.
+--
+-- These show up as a violation of one of the constraints in
+-- uids_to_x500s_clean. In that case, we still add the failing tuple to
+-- all_seen_uids_to_x500s. This lets us get a list of "suspicious tuples" by
+-- subtracting uids_to_x500s_clean from all_seen_uids_to_x500s.
+CREATE TABLE IF NOT EXISTS all_seen_uids_to_x500s
+ ( uid TEXT NOT NULL
+ , x500 TEXT NOT NULL
+ , UNIQUE(uid, x500)
+ );
diff --git a/src/bin/add-student-x500s.rs b/src/bin/add-student-x500s.rs
new file mode 100644
index 0000000..14023b1
--- /dev/null
+++ b/src/bin/add-student-x500s.rs
@@ -0,0 +1,144 @@
+use anyhow::{bail, Context, Result};
+use clap::{value_parser, ArgAction, Parser};
+use futures::{stream, FutureExt, StreamExt};
+use lambo::config::Config;
+use sqlx::sqlite::SqlitePoolOptions;
+use std::{fs, path::PathBuf};
+use stderrlog::StdErrLog;
+
+#[derive(Debug, Parser)]
+struct Args {
+ /// The path to the lambo configuration file.
+ config_path: PathBuf,
+
+ /// The path to the CSV file, as exported from Canvas's gradebook.
+ csv_path: PathBuf,
+
+ /// Decreases the log level.
+ #[clap(
+ short,
+ long,
+ conflicts_with("verbose"),
+ action = ArgAction::Count,
+ value_parser = value_parser!(u8).range(..=2)
+ )]
+ quiet: u8,
+
+ /// Increases the log level.
+ #[clap(
+ short,
+ long,
+ conflicts_with("quiet"),
+ action = ArgAction::Count,
+ value_parser = value_parser!(u8).range(..=3)
+ )]
+ verbose: u8,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Parse the arguments.
+ let args = Args::parse();
+
+ // Set up logging.
+ {
+ let mut logger = StdErrLog::new();
+ match args.quiet {
+ 0 => logger.verbosity(1 + args.verbose as usize),
+ 1 => logger.verbosity(0),
+ 2 => logger.quiet(true),
+ // UNREACHABLE: A maximum of two occurrences of quiet are allowed.
+ _ => unreachable!(),
+ };
+ // UNWRAP: No other logger should be set up.
+ logger.show_module_names(true).init().unwrap()
+ }
+
+ // Parse the config file.
+ let config = Config::read_from_file(&args.config_path)?;
+
+ // Connect to the database.
+ let db = SqlitePoolOptions::new()
+ .connect(&config.database_url)
+ .await
+ .with_context(|| format!("failed to connect to database at {:?}", config.database_url))?;
+
+ // Run any necessary migrations.
+ sqlx::migrate!().run(&db).await.with_context(|| {
+ format!(
+ "failed to run migrations on database at {:?}",
+ config.database_url
+ )
+ })?;
+
+ // Read the CSV file.
+ let csv_string = fs::read_to_string(&args.csv_path)
+ .with_context(|| format!("failed to read {:?}", args.csv_path))?;
+
+ // Skip the first two lines; Canvas puts two lines of headers in...
+ let (i, _) = csv_string
+ .match_indices('\n')
+ .nth(1)
+ .context("invalid CSV file (not enough lines)")?;
+ let csv_string = &csv_string[i..];
+
+ // Parse the CSV file.
+ let mut csv = csv::ReaderBuilder::new()
+ .has_headers(false)
+ .from_reader(csv_string.as_bytes())
+ .records()
+ .collect::<Result<Vec<_>, _>>()
+ .with_context(|| format!("failed to parse {:?}", args.csv_path))?;
+
+ // Remove the test student.
+ csv.retain(|record| &record[0] != "Student, Test");
+
+ // Collect all the X.500s, checking that they were of the right form.
+ let x500s = csv
+ .into_iter()
+ .map(|record| {
+ let email = &record[3];
+ if let Some(x500) = email.strip_suffix("@umn.edu") {
+ Ok(x500.to_string())
+ } else {
+ bail!("not a valid UMN email: {:?}", email)
+ }
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ // Insert them all in the database.
+ //
+ // Looks like sqlx doesn't actually have bulk insert? WTF?
+ //
+ // https://github.com/launchbadge/sqlx/issues/294
+ let db = &db;
+ let errors = stream::iter(x500s)
+ .map(|x500| async move {
+ sqlx::query!(
+ "INSERT OR IGNORE INTO student_x500s (x500) VALUES (?)",
+ x500
+ )
+ .execute(db)
+ .await
+ .context("failed to insert X.500s")
+ })
+ .filter_map(|future| future.map(|r| r.err()))
+ .collect::<Vec<_>>()
+ .await;
+ if !errors.is_empty() {
+ log::error!("encountered {} errors:", errors.len());
+ for error in errors {
+ log::error!("{:?}", error);
+ }
+ bail!("failed to insert X.500s")
+ }
+
+ // Count the number of X.500s.
+ let x500_count = sqlx::query!("SELECT COUNT(x500) as count from student_x500s")
+ .fetch_one(db)
+ .await
+ .context("failed to get a count of X.500s")?;
+ log::info!("We now have {} student X.500s", x500_count.count);
+
+ Ok(())
+}
diff --git a/src/main.rs b/src/bin/lambo.rs
index c9dbd7a..e3e81a6 100644
--- a/src/main.rs
+++ b/src/bin/lambo.rs
@@ -1,26 +1,14 @@
-mod handlers;
-
-use crate::handlers::*;
use anyhow::{Context as _, Result};
use clap::{value_parser, ArgAction, Parser};
-use serde::Deserialize;
-use serenity::{
- all::{ActivityData, GatewayIntents, GuildMemberUpdateEvent, Member, Ready},
- async_trait,
- client::{Context, EventHandler},
- Client,
-};
-use std::{fs, path::PathBuf};
+use lambo::{config::Config, handlers::*};
+use serenity::{all::GatewayIntents, Client};
+use sqlx::sqlite::SqlitePoolOptions;
+use std::path::PathBuf;
use stderrlog::StdErrLog;
-#[derive(Debug, Deserialize)]
-struct Config {
- discord_token: String,
-}
-
#[derive(Debug, Parser)]
struct Args {
- /// The path to the configuration file.
+ /// The path to the lambo configuration file.
config_path: PathBuf,
/// Decreases the log level.
@@ -46,6 +34,7 @@ struct Args {
#[tokio::main]
async fn main() -> Result<()> {
+ // Parse the arguments.
let args = Args::parse();
// Set up logging.
@@ -62,23 +51,32 @@ async fn main() -> Result<()> {
logger.show_module_names(true).init().unwrap()
}
- let config_str = fs::read_to_string(&args.config_path)
- .with_context(|| format!("failed to read {}", args.config_path.display()))?;
- let config: Config = toml::from_str(&config_str)
- .with_context(|| format!("failed to parse {}", args.config_path.display()))?;
- drop(config_str);
+ // Parse the config file.
+ let config = Config::read_from_file(&args.config_path)?;
- let handler = MultiHandler(vec![Box::new(PresenceSetter), Box::new(X500Mapper)]);
+ // Connect to the database.
+ let db = SqlitePoolOptions::new()
+ .connect(&config.database_url)
+ .await
+ .with_context(|| format!("failed to connect to database at {:?}", config.database_url))?;
+
+ // Run any necessary migrations.
+ sqlx::migrate!().run(&db).await.with_context(|| {
+ format!(
+ "failed to run migrations on database at {:?}",
+ config.database_url
+ )
+ })?;
- let mut client = Client::builder(
- &config.discord_token,
- GatewayIntents::default() | GatewayIntents::GUILD_MEMBERS,
- )
- .event_handler(handler)
- .await
- .context("failed to create Discord client")?;
+ // Create the handlers.
+ let handler = MultiHandler(vec![Box::new(PresenceSetter), Box::new(X500Mapper(db))]);
- client
+ // Start up the client.
+ let intents = GatewayIntents::default() | GatewayIntents::GUILD_MEMBERS;
+ Client::builder(&config.discord_token, intents)
+ .event_handler(handler)
+ .await
+ .context("failed to create Discord client")?
.start()
.await
.context("failed to start Discord client")?;
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..14a4c19
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,17 @@
+use anyhow::{Context, Result};
+use serde::Deserialize;
+use std::{fs, path::Path};
+
+#[derive(Debug, Deserialize)]
+pub struct Config {
+ pub database_url: String,
+ pub discord_token: String,
+}
+
+impl Config {
+ pub fn read_from_file(path: &Path) -> Result<Config> {
+ let config_str = fs::read_to_string(path)
+ .with_context(|| format!("failed to read {}", path.display()))?;
+ toml::from_str(&config_str).with_context(|| format!("failed to parse {}", path.display()))
+ }
+}
diff --git a/src/handlers/x500_mapper.rs b/src/handlers/x500_mapper.rs
index f477e97..1dd8955 100644
--- a/src/handlers/x500_mapper.rs
+++ b/src/handlers/x500_mapper.rs
@@ -1,22 +1,31 @@
use serenity::{
- all::{GuildMemberUpdateEvent, Member},
+ all::{GuildMemberUpdateEvent, Member, UserId},
async_trait,
client::{Context, EventHandler},
};
+use sqlx::{Database, Pool};
/// A handler that notices people with an X.500 in their nicknames that matches a student's, and
/// records it in the database.
-pub struct X500Mapper;
+pub struct X500Mapper<DB: Database>(pub Pool<DB>);
+
+impl<DB: Database> X500Mapper<DB> {
+ async fn notice_member(&self, nick: &str, uid: UserId) {
+ dbg!((nick, uid));
+ }
+}
#[async_trait]
-impl EventHandler for X500Mapper {
+impl<DB: Database> EventHandler for X500Mapper<DB> {
async fn guild_member_update(
&self,
_ctx: Context,
- old_if_available: Option<Member>,
- new: Option<Member>,
+ _old_if_available: Option<Member>,
+ _new: Option<Member>,
event: GuildMemberUpdateEvent,
) {
- dbg!((old_if_available, new, event));
+ if let Some(nick) = event.nick {
+ self.notice_member(&nick, event.user.id).await
+ }
}
}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..61d095d
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod config;
+pub mod handlers;