First merge #1
|
@ -0,0 +1,2 @@
|
|||
.woodpecker/
|
||||
target/
|
|
@ -24,3 +24,4 @@ rust-project.json
|
|||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer
|
||||
|
||||
*.db*
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
platform: 'linux/amd64'
|
||||
|
||||
branches:
|
||||
exclude: [main]
|
||||
|
||||
pipeline:
|
||||
build:
|
||||
image: 'rust:1.69'
|
||||
commands:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
|
@ -0,0 +1,11 @@
|
|||
platform: 'linux/amd64'
|
||||
|
||||
branches:
|
||||
exclude: [main]
|
||||
|
||||
pipeline:
|
||||
clippy:
|
||||
image: 'rust:1.69'
|
||||
commands:
|
||||
- rustup component add clippy
|
||||
- cargo clippy -- --no-deps -Dwarnings
|
|
@ -0,0 +1,22 @@
|
|||
platform: 'linux/amd64'
|
||||
branches: 'main'
|
||||
|
||||
pipeline:
|
||||
release:
|
||||
image: 'plugins/docker'
|
||||
settings:
|
||||
registry: 'git.rustybever.be'
|
||||
repo: 'git.rustybever.be/chewing_bever/affy'
|
||||
tag:
|
||||
- 'latest'
|
||||
mtu: 1300
|
||||
secrets:
|
||||
- 'docker_username'
|
||||
- 'docker_password'
|
||||
|
||||
deploy:
|
||||
image: 'curlimages/curl'
|
||||
secrets:
|
||||
- 'webhook'
|
||||
commands:
|
||||
- curl -XPOST --fail -s "$WEBHOOK"
|
|
@ -0,0 +1,11 @@
|
|||
platform: 'linux/amd64'
|
||||
|
||||
branches:
|
||||
exclude: [main]
|
||||
|
||||
pipeline:
|
||||
lint:
|
||||
image: 'rust:1.69'
|
||||
commands:
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt -- --check
|
|
@ -18,12 +18,26 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "affluences-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"affluences-api",
|
||||
"clap",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "affy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"affluences-api",
|
||||
"async-minecraft-ping",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"libsqlite3-sys",
|
||||
"poise",
|
||||
"tokio",
|
||||
"uuid",
|
||||
|
@ -38,6 +52,68 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is-terminal",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-minecraft-ping"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668b459c14dd8d9ef21e296af3f2a3651ff7dc3536e092fb0b09e528daaa6d89"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.68"
|
||||
|
@ -144,6 +220,48 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
|
@ -154,6 +272,12 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
|
@ -292,6 +416,40 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373"
|
||||
dependencies = [
|
||||
"diesel_derives",
|
||||
"libsqlite3-sys",
|
||||
"r2d2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_derives"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad74fdcf086be3d4fdd142f67937678fe60ed431c3b2f08599e7687269410c4"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel_migrations"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9ae22beef5e9d6fab9225ddb073c1c6c1a7a6ded5019d5da11d1e5c5adc34e2"
|
||||
dependencies = [
|
||||
"diesel",
|
||||
"migrations_internals",
|
||||
"migrations_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.6"
|
||||
|
@ -311,6 +469,27 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
|
||||
dependencies = [
|
||||
"errno-dragonfly",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno-dragonfly"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.26"
|
||||
|
@ -459,6 +638,12 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.2.6"
|
||||
|
@ -468,6 +653,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
|
@ -589,12 +780,35 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.1",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.1",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.6"
|
||||
|
@ -616,6 +830,17 @@ version = "0.2.144"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.8"
|
||||
|
@ -625,6 +850,12 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.9"
|
||||
|
@ -650,6 +881,27 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "migrations_internals"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c493c09323068c01e54c685f7da41a9ccf9219735c3766fbfd6099806ea08fbc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "migrations_macros"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a8ff27a350511de30cdabb77147501c36ef02e0451d957abea2f30caffb2b58"
|
||||
dependencies = [
|
||||
"migrations_internals",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
@ -712,7 +964,7 @@ version = "1.15.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.2.6",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
@ -772,6 +1024,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||
|
||||
[[package]]
|
||||
name = "poise"
|
||||
version = "0.5.5"
|
||||
|
@ -809,6 +1067,30 @@ version = "0.2.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.56"
|
||||
|
@ -827,6 +1109,17 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r2d2"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
|
||||
dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
|
@ -938,6 +1231,20 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.37.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.20.8"
|
||||
|
@ -971,6 +1278,15 @@ version = "1.0.13"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
|
@ -1294,6 +1610,15 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
|
@ -1432,6 +1757,12 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.2"
|
||||
|
@ -1441,6 +1772,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
|
24
Cargo.toml
24
Cargo.toml
|
@ -2,5 +2,27 @@
|
|||
|
||||
members = [
|
||||
"affluences-api",
|
||||
"bot",
|
||||
"affluences-cli",
|
||||
]
|
||||
# Don't build the CLI tool by default
|
||||
default-members = [
|
||||
".",
|
||||
"affluences-api",
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "affy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
affluences-api = { path = "./affluences-api" }
|
||||
tokio = { version = "1.28.1", features = ["full"] }
|
||||
chrono = "*"
|
||||
uuid = "*"
|
||||
poise = "0.5.5"
|
||||
async-minecraft-ping = "0.8.0"
|
||||
diesel = { version = "2.0.4", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2"] }
|
||||
diesel_migrations = { version = "2.0.0", features = [ "sqlite" ] }
|
||||
# Force sqlite3 to be bundled, allowing for a fully static binary
|
||||
libsqlite3-sys = { version = "*", features = ["bundled"] }
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
FROM rust:1.69-alpine AS builder
|
||||
|
||||
ARG DI_VER=1.2.5
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache build-base curl
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Build dumb-init
|
||||
RUN curl -Lo - "https://github.com/Yelp/dumb-init/archive/refs/tags/v${DI_VER}.tar.gz" | tar -xzf - && \
|
||||
cd "dumb-init-${DI_VER}" && \
|
||||
make SHELL=/bin/sh && \
|
||||
mv dumb-init ..
|
||||
|
||||
WORKDIR /build/affy
|
||||
|
||||
COPY ./ ./
|
||||
|
||||
RUN cargo build --release && \
|
||||
[ "$(readelf -d target/release/affy | grep NEEDED | wc -l)" = 0 ]
|
||||
|
||||
|
||||
FROM busybox:1.36.0
|
||||
|
||||
COPY --from=builder /build/dumb-init /build/affy/target/release/affy /bin/
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
ENV TZ=Europe/Brussels
|
||||
|
||||
USER www-data:www-data
|
||||
|
||||
ENTRYPOINT ["/bin/dumb-init", "--"]
|
||||
CMD ["/bin/affy"]
|
|
@ -1,3 +1,45 @@
|
|||
# Affluences API Reference
|
||||
|
||||
## Terminology
|
||||
|
||||
* `site`
|
||||
* General name for a location/institution
|
||||
* Defined by a UUID and a slug, e.g. `ghent-university`
|
||||
* Can contain one more or more sites, e.g.
|
||||
`ghent-university-library-book-tower` is a child of `ghent-university`
|
||||
|
||||
## Notes
|
||||
|
||||
The API checks for browser user agents, so your requests should include a valid
|
||||
user agent of a modern browser.
|
||||
|
||||
## API Routes
|
||||
|
||||
### Search for sites
|
||||
|
||||
`GET https://api.affluences.com/app/v3/sites`
|
||||
|
||||
**Body format**
|
||||
|
||||
```json
|
||||
{
|
||||
"selected_categories": [
|
||||
1
|
||||
],
|
||||
"page": 0,
|
||||
"search_query": "university of ghent"
|
||||
}
|
||||
```
|
||||
|
||||
**Response format**
|
||||
|
||||
`Data<Vec<SiteData>>`
|
||||
|
||||
### Retrieve time table for a given site
|
||||
|
||||
`GET https://reservation.affluences.com/api/sites/4737e57a-ee05-4f7b-901a-7bb541eeb297`
|
||||
|
||||
|
||||
curl -L 'https://api.affluences.com/app/v3/sites/ghent-university' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0'
|
||||
|
||||
returnt lijst van alle bibliotheken op ugent, inclusief de uuids die dan nodig zijn om de specifieke requests te sturen naar die bib zijn stuff
|
||||
|
|
|
@ -20,6 +20,23 @@ impl AffluencesClient {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: String) -> reqwest::Result<SiteSearchResponse> {
|
||||
let url = "https://api.affluences.com/app/v3/sites";
|
||||
let body = SiteSearch {
|
||||
search_query: query,
|
||||
};
|
||||
|
||||
Ok(self
|
||||
.client
|
||||
.post(url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.json::<Data<SiteSearchResponse>>()
|
||||
.await?
|
||||
.data)
|
||||
}
|
||||
|
||||
pub async fn available(
|
||||
&self,
|
||||
site_id: uuid::Uuid,
|
||||
|
@ -72,3 +89,9 @@ impl AffluencesClient {
|
|||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AffluencesClient {
|
||||
fn default() -> Self {
|
||||
AffluencesClient::new()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::hh_mm_time_format;
|
||||
use chrono::NaiveTime;
|
||||
use chrono::{Duration, NaiveTime};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Copy)]
|
||||
|
@ -41,3 +41,45 @@ pub struct Resource {
|
|||
pub slots_state: u32,
|
||||
pub hours: Vec<HourBlock>,
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
pub fn condensed_hours(&self) -> Vec<(&HourBlock, Duration)> {
|
||||
if self.hours.is_empty() {
|
||||
return Default::default();
|
||||
}
|
||||
|
||||
let mut start_hour = self.hours.first().unwrap();
|
||||
let mut duration = Duration::minutes(start_hour.granularity.into());
|
||||
let mut out: Vec<(&HourBlock, Duration)> = Default::default();
|
||||
|
||||
for hour in self.hours.iter().skip(1) {
|
||||
if hour.state == start_hour.state {
|
||||
duration = duration + Duration::minutes(hour.granularity.into());
|
||||
} else {
|
||||
out.push((start_hour, duration));
|
||||
start_hour = hour;
|
||||
duration = Duration::minutes(hour.granularity.into());
|
||||
}
|
||||
}
|
||||
|
||||
out.push((start_hour, duration));
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn condensed_available_hours(&self) -> Vec<(&HourBlock, Duration)> {
|
||||
self.condensed_hours()
|
||||
.into_iter()
|
||||
.filter(|(hour, _)| hour.state == 1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns whether a slot with the given state and time bounds is present in the list of
|
||||
/// hours.
|
||||
pub fn has_slot(&self, start_time: NaiveTime, end_time: NaiveTime, state: u32) -> bool {
|
||||
self.condensed_hours()
|
||||
.into_iter()
|
||||
.filter(|(block, _)| block.state == state)
|
||||
.any(|(block, duration)| start_time >= block.hour && end_time <= block.hour + duration)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use chrono::NaiveTime;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FORMAT: &'static str = "%H:%M";
|
||||
const FORMAT: &str = "%H:%M";
|
||||
|
||||
pub fn serialize<S>(time: &NaiveTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Data<T> {
|
||||
pub data: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataCategory {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
pub name_plural: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataLocationCoordinates {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataLocationAddress {
|
||||
pub route: String,
|
||||
pub city: String,
|
||||
|
@ -27,47 +27,47 @@ pub struct SiteDataLocationAddress {
|
|||
pub country_code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataLocation {
|
||||
pub coordinates: SiteDataLocationCoordinates,
|
||||
pub address: SiteDataLocationAddress,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataForecast {
|
||||
pub opened: bool,
|
||||
pub occupancy: u32,
|
||||
pub occupancy: Option<u32>,
|
||||
// waiting_time
|
||||
pub waiting_time_overflow: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataNotice {
|
||||
pub message: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataService {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataInfo {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteDataStatus {
|
||||
pub state: String,
|
||||
pub text: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteData {
|
||||
pub id: uuid::Uuid,
|
||||
pub slug: String,
|
||||
|
@ -104,3 +104,15 @@ pub struct SiteData {
|
|||
#[serde(rename = "publicationStatus")]
|
||||
pub publication_status: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct SiteSearch {
|
||||
pub search_query: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SiteSearchResponse {
|
||||
pub page: u32,
|
||||
pub max_size: u32,
|
||||
pub results: Vec<SiteData>,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
name = "affy"
|
||||
name = "affluences-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
@ -7,7 +7,6 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
affluences-api = { path = "../affluences-api" }
|
||||
clap = { version = "4.2.7", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
tokio = { version = "1.28.1", features = ["full"] }
|
||||
chrono = "*"
|
||||
uuid = "*"
|
||||
poise = "0.5.5"
|
|
@ -0,0 +1,30 @@
|
|||
use affluences_api::AffluencesClient;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// does testing things
|
||||
SearchSite { query: String },
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
let client = AffluencesClient::new();
|
||||
|
||||
match &cli.command {
|
||||
Some(Commands::SearchSite { query }) => {
|
||||
let res = client.search(query.to_string()).await.unwrap();
|
||||
let s = serde_json::to_string_pretty(&res).unwrap();
|
||||
println!("{}", s);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
|
@ -1,160 +0,0 @@
|
|||
use crate::{Context, Error};
|
||||
use affluences_api::HourBlock;
|
||||
use chrono::{Duration, NaiveDate};
|
||||
use uuid::{uuid, Uuid};
|
||||
|
||||
const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297");
|
||||
const TIME_FORMAT: &'static str = "%H:%M";
|
||||
|
||||
/// Show this help menu
|
||||
#[poise::command(prefix_command, track_edits, slash_command)]
|
||||
pub async fn help(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Specific command to show help about"]
|
||||
#[autocomplete = "poise::builtins::autocomplete_command"]
|
||||
command: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
poise::builtins::help(
|
||||
ctx,
|
||||
command.as_deref(),
|
||||
poise::builtins::HelpConfiguration {
|
||||
extra_text_at_bottom: "This is an example bot made to showcase features of my custom Discord bot framework",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Vote for something
|
||||
///
|
||||
/// Enter `~vote pumpkin` to vote for pumpkins
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn vote(
|
||||
ctx: Context<'_>,
|
||||
#[description = "What to vote for"] choice: String,
|
||||
) -> Result<(), Error> {
|
||||
// Lock the Mutex in a block {} so the Mutex isn't locked across an await point
|
||||
let num_votes = {
|
||||
let mut hash_map = ctx.data().votes.lock().unwrap();
|
||||
let num_votes = hash_map.entry(choice.clone()).or_default();
|
||||
*num_votes += 1;
|
||||
*num_votes
|
||||
};
|
||||
|
||||
let response = format!("Successfully voted for {choice}. {choice} now has {num_votes} votes!");
|
||||
ctx.say(response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve number of votes
|
||||
///
|
||||
/// Retrieve the number of votes either in general, or for a specific choice:
|
||||
/// ```
|
||||
/// ~getvotes
|
||||
/// ~getvotes pumpkin
|
||||
/// ```
|
||||
#[poise::command(prefix_command, track_edits, aliases("votes"), slash_command)]
|
||||
pub async fn getvotes(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Choice to retrieve votes for"] choice: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(choice) = choice {
|
||||
let num_votes = *ctx.data().votes.lock().unwrap().get(&choice).unwrap_or(&0);
|
||||
let response = match num_votes {
|
||||
0 => format!("Nobody has voted for {} yet", choice),
|
||||
_ => format!("{} people have voted for {}", num_votes, choice),
|
||||
};
|
||||
ctx.say(response).await?;
|
||||
} else {
|
||||
let mut response = String::new();
|
||||
for (choice, num_votes) in ctx.data().votes.lock().unwrap().iter() {
|
||||
response += &format!("{}: {} votes", choice, num_votes);
|
||||
}
|
||||
|
||||
if response.is_empty() {
|
||||
response += "Nobody has voted for anything yet :(";
|
||||
}
|
||||
|
||||
ctx.say(response).await?;
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List available timeslots for day
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> {
|
||||
let client = &ctx.data().client;
|
||||
let resources = client.available(STERRE_BIB_ID, date, 1).await?;
|
||||
let mut fields: Vec<(String, String, bool)> = Default::default();
|
||||
|
||||
for resource in &resources {
|
||||
if resource.hours.len() == 0 {
|
||||
fields.push((
|
||||
resource.resource_name.clone(),
|
||||
"Nothing available.".to_string(),
|
||||
false,
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut lines: Vec<String> = Default::default();
|
||||
|
||||
let mut start_hour_opt: Option<&HourBlock> = None;
|
||||
let mut duration = Duration::seconds(0);
|
||||
|
||||
for hour in &resource.hours {
|
||||
if let Some(start_hour) = start_hour_opt {
|
||||
if hour.state == 1 {
|
||||
duration = duration + Duration::minutes(hour.granularity.into());
|
||||
} else {
|
||||
let end_hour = start_hour.hour + duration;
|
||||
lines.push(format!(
|
||||
"{} - {} ({:02}:{:02})",
|
||||
start_hour.hour.format(TIME_FORMAT),
|
||||
end_hour.format(TIME_FORMAT),
|
||||
duration.num_hours(),
|
||||
duration.num_minutes() % 60
|
||||
));
|
||||
start_hour_opt = None;
|
||||
}
|
||||
} else if hour.state == 1 {
|
||||
start_hour_opt = Some(hour);
|
||||
duration = Duration::minutes(hour.granularity.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Print final entry if present
|
||||
if let Some(start_hour) = start_hour_opt {
|
||||
let end_hour = start_hour.hour + duration;
|
||||
lines.push(format!(
|
||||
"{} - {} ({:02}:{:02})",
|
||||
start_hour.hour.format(TIME_FORMAT),
|
||||
end_hour.format(TIME_FORMAT),
|
||||
duration.num_hours(),
|
||||
duration.num_minutes() % 60
|
||||
));
|
||||
}
|
||||
|
||||
fields.push((resource.resource_name.clone(), lines.join("\n"), false));
|
||||
}
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.description(format!("Available booking dates for {}.", date))
|
||||
.fields(fields)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create a reservation
|
||||
// #[poise::command(prefix_command, slash_command)]
|
||||
// pub async fn reserve(
|
||||
// ctx: Context<'_>,
|
||||
// date: NaiveDate,
|
||||
// ) -> Result<(), Error> {
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/db/schema.rs"
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
|
@ -0,0 +1,2 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE users;
|
|
@ -0,0 +1,11 @@
|
|||
-- Your SQL goes here
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
discord_id UNSIGNED BIG INT NOT NULL,
|
||||
guild_id UNSIGNED BIG INT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
|
||||
UNIQUE(discord_id, guild_id)
|
||||
);
|
|
@ -0,0 +1,165 @@
|
|||
use crate::commands::{EmbedField, HumanNaiveDate};
|
||||
use crate::db::users::User;
|
||||
use crate::{Context, Error};
|
||||
|
||||
use affluences_api::{Reservation, Resource};
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use uuid::{uuid, Uuid};
|
||||
|
||||
const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297");
|
||||
const TIME_FORMAT: &str = "%H:%M";
|
||||
|
||||
/// Parent command for all bib-related commands
|
||||
///
|
||||
/// Commands for reservating study rooms in the bib.
|
||||
#[poise::command(prefix_command, slash_command, subcommands("available", "book"))]
|
||||
pub async fn bib(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resource_to_embed_field(resource: Resource) -> EmbedField {
|
||||
let available_hours = resource.condensed_available_hours();
|
||||
let title = format!("{} ({}p)", resource.resource_name, resource.capacity);
|
||||
|
||||
if available_hours.is_empty() {
|
||||
(title, "Nothing available.".to_string(), false)
|
||||
} else {
|
||||
(
|
||||
title,
|
||||
available_hours
|
||||
.into_iter()
|
||||
.map(|(start_block, duration)| {
|
||||
format!(
|
||||
"{} - {} ({:02}:{:02})",
|
||||
start_block.hour.format(TIME_FORMAT),
|
||||
(start_block.hour + duration).format(TIME_FORMAT),
|
||||
duration.num_hours(),
|
||||
duration.num_minutes() % 60
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// List available timeslots for day
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn available(ctx: Context<'_>, date: HumanNaiveDate) -> Result<(), Error> {
|
||||
let client = &ctx.data().client;
|
||||
let mut resources = client
|
||||
.available(STERRE_BIB_ID, date.clone().into(), 1)
|
||||
.await?;
|
||||
// Cloning here isn't super efficient, but this list only consists of a handful of elements so
|
||||
// it's fine
|
||||
resources.sort_by_key(|k| k.resource_name.clone());
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.description(format!(
|
||||
"Available booking dates for {}.",
|
||||
Into::<NaiveDate>::into(date)
|
||||
))
|
||||
.fields(
|
||||
resources
|
||||
.into_iter()
|
||||
.map(resource_to_embed_field)
|
||||
.collect::<Vec<EmbedField>>(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn book(
|
||||
ctx: Context<'_>,
|
||||
date: HumanNaiveDate,
|
||||
start_time: NaiveTime,
|
||||
end_time: NaiveTime,
|
||||
#[description = "Minimum seats the room should have."] capacity: Option<u32>,
|
||||
) -> Result<(), Error> {
|
||||
if ctx.guild_id().is_none() {
|
||||
ctx.say("You have to send this message from a guild.")
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let guild_id = ctx.guild_id().unwrap();
|
||||
let discord_id = ctx.author().id.0 as i64;
|
||||
|
||||
let user = {
|
||||
let mut conn = ctx.data().pool.get()?;
|
||||
User::get(&mut conn, guild_id.into(), discord_id)?
|
||||
};
|
||||
|
||||
if user.is_none() {
|
||||
ctx.say("You have to register before being able to book reservations.")
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user = user.unwrap();
|
||||
|
||||
let client = &ctx.data().client;
|
||||
let resources = client
|
||||
.available(STERRE_BIB_ID, date.clone().into(), 1)
|
||||
.await?;
|
||||
let chosen_resource = resources
|
||||
.iter()
|
||||
.filter(|r| capacity.is_none() || capacity.unwrap() <= r.capacity)
|
||||
.find(|r| r.has_slot(start_time, end_time, 1));
|
||||
|
||||
if let Some(chosen_resource) = chosen_resource {
|
||||
let reservation = Reservation {
|
||||
auth_type: None,
|
||||
email: user.email.clone(),
|
||||
date: date.clone().into(),
|
||||
start_time,
|
||||
end_time,
|
||||
note: "coworking space".to_string(),
|
||||
user_firstname: user.first_name,
|
||||
user_lastname: user.last_name,
|
||||
user_phone: None,
|
||||
person_count: capacity.unwrap_or(1),
|
||||
};
|
||||
|
||||
client
|
||||
.make_reservation(chosen_resource.resource_id, &reservation)
|
||||
.await?;
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.description("A new reservation has been made.")
|
||||
.field("when", format!("{} {} - {}", Into::<NaiveDate>::into(date), start_time.format(TIME_FORMAT), end_time.format(TIME_FORMAT)), false)
|
||||
.field("where", &chosen_resource.resource_name, false)
|
||||
.footer(|ft| ft.text(
|
||||
format!("A confirmation mail has been sent to {}. Please check your email and confirm your reservation within two hours.", user.email)))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say("No slot is available within your requested bounds.")
|
||||
.await?;
|
||||
}
|
||||
|
||||
// let resources = if let Some(capacity) = capacity {
|
||||
// resources.filter(|r| r.capacity >= capacity)
|
||||
// };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Create a reservation
|
||||
// #[poise::command(prefix_command, slash_command)]
|
||||
// pub async fn reserve(
|
||||
// ctx: Context<'_>,
|
||||
// date: NaiveDate,
|
||||
// ) -> Result<(), Error> {
|
||||
|
||||
// }
|
|
@ -0,0 +1,55 @@
|
|||
use crate::{Context, Error};
|
||||
use async_minecraft_ping::ServerDescription;
|
||||
|
||||
const DEFAULT_SERVER: &str = "rustybever.be";
|
||||
|
||||
/// Parent command for Minecraft-related actions.
|
||||
///
|
||||
/// Minecraft-related commands
|
||||
#[poise::command(prefix_command, slash_command, subcommands("ping"))]
|
||||
pub async fn mc(_ctx: Context<'_>) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ping a minecraft server; defaults to our server.
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn ping(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Address of the server"] address: Option<String>,
|
||||
#[description = "Port the server runs on"] port: Option<u16>,
|
||||
) -> Result<(), Error> {
|
||||
let mut full_name = address.unwrap_or(DEFAULT_SERVER.to_string());
|
||||
let mut builder = async_minecraft_ping::ConnectionConfig::build(&full_name);
|
||||
|
||||
if let Some(port) = port {
|
||||
builder = builder.with_port(port);
|
||||
full_name += &format!(":{}", port);
|
||||
}
|
||||
|
||||
let conn = builder.connect().await?;
|
||||
let status = conn.status().await?.status;
|
||||
|
||||
let description = match status.description {
|
||||
ServerDescription::Plain(s) => s,
|
||||
ServerDescription::Object { text } => text,
|
||||
};
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.description(format!("Server information for {}", full_name))
|
||||
.field("version", status.version.name, false)
|
||||
.field("description", description, false)
|
||||
.field(
|
||||
"players",
|
||||
format!(
|
||||
"{} of {} player(s) online",
|
||||
status.players.online, status.players.max
|
||||
),
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
mod bib;
|
||||
mod minecraft;
|
||||
mod users;
|
||||
|
||||
use chrono::Datelike;
|
||||
use core::str;
|
||||
|
||||
use crate::{Context, Data, Error};
|
||||
|
||||
type EmbedField = (String, String, bool);
|
||||
|
||||
const DAY_TERMS: [&str; 3] = ["today", "tomorrow", "overmorrow"];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HumanNaiveDate(chrono::NaiveDate);
|
||||
|
||||
impl str::FromStr for HumanNaiveDate {
|
||||
type Err = chrono::format::ParseError;
|
||||
|
||||
fn from_str(s: &str) -> chrono::format::ParseResult<Self> {
|
||||
if let Some(days_to_add) = DAY_TERMS.iter().position(|term| s.to_lowercase() == *term) {
|
||||
let now = chrono::Local::now().naive_local().date();
|
||||
|
||||
// days_to_add will never be greater than 2
|
||||
Ok(HumanNaiveDate(
|
||||
now + chrono::Duration::days(days_to_add.try_into().unwrap()),
|
||||
))
|
||||
} else if let Ok(weekday) = s.parse::<chrono::Weekday>() {
|
||||
let now = chrono::Local::now().naive_local().date();
|
||||
let cur_day = now.weekday();
|
||||
let cur_day_index = cur_day.num_days_from_monday();
|
||||
let parsed_day_index = weekday.num_days_from_monday();
|
||||
|
||||
let days_to_add = if cur_day_index <= parsed_day_index {
|
||||
parsed_day_index - cur_day_index
|
||||
} else {
|
||||
7 - (cur_day_index - parsed_day_index)
|
||||
};
|
||||
|
||||
Ok(HumanNaiveDate(
|
||||
now + chrono::Duration::days(days_to_add.into()),
|
||||
))
|
||||
} else {
|
||||
chrono::NaiveDate::from_str(s).map(HumanNaiveDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HumanNaiveDate> for chrono::NaiveDate {
|
||||
fn from(val: HumanNaiveDate) -> chrono::NaiveDate {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commands() -> Vec<poise::structs::Command<Data, Error>> {
|
||||
vec![
|
||||
help(),
|
||||
bib::bib(),
|
||||
minecraft::mc(),
|
||||
users::register(),
|
||||
users::registered(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Show this help menu
|
||||
#[poise::command(prefix_command, track_edits, slash_command)]
|
||||
pub async fn help(
|
||||
ctx: Context<'_>,
|
||||
#[description = "Specific command to show help about"]
|
||||
#[autocomplete = "poise::builtins::autocomplete_command"]
|
||||
command: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
poise::builtins::help(
|
||||
ctx,
|
||||
command.as_deref(),
|
||||
poise::builtins::HelpConfiguration {
|
||||
extra_text_at_bottom: "Brought to you by Doofenshmirtz Evil Incorporated.",
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
use crate::db::users::{NewUser, User};
|
||||
use crate::{Context, Error};
|
||||
use diesel::RunQueryDsl;
|
||||
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn register(
|
||||
ctx: Context<'_>,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
email: String,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(guild_id) = ctx.guild_id() {
|
||||
let discord_id = ctx.author().id.0 as i64;
|
||||
|
||||
{
|
||||
let mut conn = ctx.data().pool.get()?;
|
||||
|
||||
if User::get(&mut conn, guild_id.into(), discord_id)?.is_some() {
|
||||
ctx.say("You've already been registered.").await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_user = NewUser {
|
||||
discord_id,
|
||||
guild_id: guild_id.into(),
|
||||
first_name,
|
||||
last_name,
|
||||
email,
|
||||
};
|
||||
|
||||
new_user.insert(&mut conn)?;
|
||||
}
|
||||
|
||||
ctx.say("You have been registered.").await?;
|
||||
} else {
|
||||
ctx.say("You have to send this message from a guild.")
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn registered(ctx: Context<'_>) -> Result<(), Error> {
|
||||
if let Some(guild_id) = ctx.guild_id() {
|
||||
let users = {
|
||||
let mut conn = ctx.data().pool.get()?;
|
||||
User::by_guild_id(guild_id.into()).load(&mut conn)?
|
||||
};
|
||||
|
||||
ctx.send(|f| {
|
||||
f.embed(|e| {
|
||||
e.description("Registered users").fields(
|
||||
users
|
||||
.into_iter()
|
||||
.map(|u| (format!("{} {}", u.first_name, u.last_name), u.email, false)),
|
||||
)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
ctx.say("You are not in a guild.").await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
mod schema;
|
||||
pub mod users;
|
||||
|
||||
use diesel::connection::SimpleConnection;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::sqlite::{Sqlite, SqliteConnection};
|
||||
use std::error::Error;
|
||||
|
||||
use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness};
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
type DbError = Box<dyn Error + Send + Sync + 'static>;
|
||||
|
||||
fn run_migrations(connection: &mut impl MigrationHarness<Sqlite>) -> Result<(), DbError> {
|
||||
// This will run the necessary migrations.
|
||||
//
|
||||
// See the documentation for `MigrationHarness` for
|
||||
// all available methods.
|
||||
connection.run_pending_migrations(MIGRATIONS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_db(conn: &mut SqliteConnection) -> Result<(), DbError> {
|
||||
// Enable WAL mode and enforce foreign keys
|
||||
conn.batch_execute(
|
||||
"PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;",
|
||||
)?;
|
||||
run_migrations(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_pool(url: &str) -> Result<Pool<ConnectionManager<SqliteConnection>>, DbError> {
|
||||
let manager = ConnectionManager::new(url);
|
||||
|
||||
let pool = Pool::builder().test_on_check_out(true).build(manager)?;
|
||||
|
||||
let mut conn = pool.get()?;
|
||||
initialize_db(&mut conn)?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
users (id) {
|
||||
id -> Integer,
|
||||
discord_id -> BigInt,
|
||||
guild_id -> BigInt,
|
||||
email -> Text,
|
||||
first_name -> Text,
|
||||
last_name -> Text,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use super::schema::users::{self, dsl::*};
|
||||
use diesel::dsl::Eq;
|
||||
use diesel::dsl::{AsSelect, Select};
|
||||
use diesel::helper_types::Filter;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::Sqlite;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
|
||||
#[derive(Queryable, Selectable, AsChangeset)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub discord_id: i64,
|
||||
pub guild_id: i64,
|
||||
pub email: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[diesel(table_name = users)]
|
||||
pub struct NewUser {
|
||||
pub discord_id: i64,
|
||||
pub guild_id: i64,
|
||||
pub email: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
}
|
||||
|
||||
type All = Select<users::table, AsSelect<User, Sqlite>>;
|
||||
type WithGuild<T> = Eq<guild_id, T>;
|
||||
type ByGuild<T> = Filter<All, WithGuild<T>>;
|
||||
|
||||
impl User {
|
||||
pub fn all() -> All {
|
||||
users::table.select(User::as_select())
|
||||
}
|
||||
|
||||
// pub fn by_guild<T>(guild_id_: T) -> ByGuild<T>
|
||||
// where T: AsExpression<BigInt>
|
||||
// {
|
||||
// Self::all().filter(guild_id.eq(guild_id_))
|
||||
// }
|
||||
|
||||
pub fn by_guild_id(guild_id_: i64) -> ByGuild<i64> {
|
||||
Self::all().filter(guild_id.eq(guild_id_))
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
conn: &mut SqliteConnection,
|
||||
guild_id_: i64,
|
||||
discord_id_: i64,
|
||||
) -> Result<Option<User>, diesel::result::Error> {
|
||||
Self::all()
|
||||
.filter(guild_id.eq(guild_id_))
|
||||
.filter(discord_id.eq(discord_id_))
|
||||
.first(conn)
|
||||
.optional()
|
||||
}
|
||||
|
||||
pub fn get_by_id(
|
||||
conn: &mut SqliteConnection,
|
||||
id_: i32,
|
||||
) -> Result<Option<User>, diesel::result::Error> {
|
||||
Self::all().find(id_).first(conn).optional()
|
||||
}
|
||||
|
||||
pub fn update(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
|
||||
diesel::update(users::table).set(self).execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl NewUser {
|
||||
pub fn insert(&self, conn: &mut SqliteConnection) -> Result<User, diesel::result::Error> {
|
||||
diesel::insert_into(users::table)
|
||||
.values(self)
|
||||
.get_result(conn)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
mod commands;
|
||||
mod db;
|
||||
|
||||
use affluences_api::AffluencesClient;
|
||||
use diesel::r2d2::{ConnectionManager, Pool};
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use std::{collections::HashMap, env::var, sync::Mutex, time::Duration};
|
||||
use std::{env::var, time::Duration};
|
||||
|
||||
// Types used by all command functions
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
@ -10,8 +13,8 @@ type Context<'a> = poise::Context<'a, Data, Error>;
|
|||
|
||||
// Custom user data passed to all command functions
|
||||
pub struct Data {
|
||||
votes: Mutex<HashMap<String, u32>>,
|
||||
client: AffluencesClient,
|
||||
pool: Pool<ConnectionManager<SqliteConnection>>,
|
||||
}
|
||||
|
||||
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
|
||||
|
@ -36,12 +39,7 @@ async fn main() {
|
|||
// FrameworkOptions contains all of poise's configuration option in one struct
|
||||
// Every option can be omitted to use its default value
|
||||
let options = poise::FrameworkOptions {
|
||||
commands: vec![
|
||||
commands::help(),
|
||||
commands::vote(),
|
||||
commands::getvotes(),
|
||||
commands::available(),
|
||||
],
|
||||
commands: commands::commands(),
|
||||
prefix_options: poise::PrefixFrameworkOptions {
|
||||
prefix: Some("~".into()),
|
||||
edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))),
|
||||
|
@ -86,6 +84,8 @@ async fn main() {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let pool = db::initialize_pool("affy.db").expect("Failed to initialize database.");
|
||||
|
||||
poise::Framework::builder()
|
||||
.token(
|
||||
var("DISCORD_TOKEN")
|
||||
|
@ -96,8 +96,8 @@ async fn main() {
|
|||
println!("Logged in as {}", _ready.user.name);
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
Ok(Data {
|
||||
votes: Mutex::new(HashMap::new()),
|
||||
client: AffluencesClient::new(),
|
||||
pool,
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue