First merge #1

Merged
Jef Roosens merged 26 commits from dev into main 2023-05-17 23:33:39 +02:00
30 changed files with 1159 additions and 190 deletions

2
.dockerignore 100644
View File

@ -0,0 +1,2 @@
.woodpecker/
target/

1
.env 100644
View File

@ -0,0 +1 @@
DATABASE_URL=sqlite://affy.db

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ rust-project.json
# End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer # End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer
*.db*

View File

@ -0,0 +1,11 @@
platform: 'linux/amd64'
branches:
exclude: [main]
pipeline:
build:
image: 'rust:1.69'
commands:
- cargo build --verbose
- cargo test --verbose

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1,11 @@
platform: 'linux/amd64'
branches:
exclude: [main]
pipeline:
lint:
image: 'rust:1.69'
commands:
- rustup component add rustfmt
- cargo fmt -- --check

339
Cargo.lock generated
View File

@ -18,12 +18,26 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "affluences-cli"
version = "0.1.0"
dependencies = [
"affluences-api",
"clap",
"serde_json",
"tokio",
]
[[package]] [[package]]
name = "affy" name = "affy"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"affluences-api", "affluences-api",
"async-minecraft-ping",
"chrono", "chrono",
"diesel",
"diesel_migrations",
"libsqlite3-sys",
"poise", "poise",
"tokio", "tokio",
"uuid", "uuid",
@ -38,6 +52,68 @@ dependencies = [
"libc", "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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.68" version = "0.1.68"
@ -144,6 +220,48 @@ dependencies = [
"winapi", "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]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -154,6 +272,12 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.4" version = "0.8.4"
@ -292,6 +416,40 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "digest" name = "digest"
version = "0.10.6" version = "0.10.6"
@ -311,6 +469,27 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.26" version = "1.0.26"
@ -459,6 +638,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.2.6"
@ -468,6 +653,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@ -589,12 +780,35 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.7.2" version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" 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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.6" version = "1.0.6"
@ -616,6 +830,17 @@ version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 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]] [[package]]
name = "link-cplusplus" name = "link-cplusplus"
version = "1.0.8" version = "1.0.8"
@ -625,6 +850,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.9" version = "0.4.9"
@ -650,6 +881,27 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 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]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -712,7 +964,7 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.2.6",
"libc", "libc",
] ]
@ -772,6 +1024,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]] [[package]]
name = "poise" name = "poise"
version = "0.5.5" version = "0.5.5"
@ -809,6 +1067,30 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.56" version = "1.0.56"
@ -827,6 +1109,17 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -938,6 +1231,20 @@ dependencies = [
"winapi", "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]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.8" version = "0.20.8"
@ -971,6 +1278,15 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -1294,6 +1610,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.2" version = "0.3.2"
@ -1432,6 +1757,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.3.2" version = "1.3.2"
@ -1441,6 +1772,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"

View File

@ -2,5 +2,27 @@
members = [ members = [
"affluences-api", "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"] }

35
Dockerfile 100644
View File

@ -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"]

View File

@ -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' 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 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

View File

@ -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( pub async fn available(
&self, &self,
site_id: uuid::Uuid, site_id: uuid::Uuid,
@ -72,3 +89,9 @@ impl AffluencesClient {
.await .await
} }
} }
impl Default for AffluencesClient {
fn default() -> Self {
AffluencesClient::new()
}
}

View File

@ -1,5 +1,5 @@
use super::hh_mm_time_format; use super::hh_mm_time_format;
use chrono::NaiveTime; use chrono::{Duration, NaiveTime};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize, Debug, Clone, Copy)] #[derive(Deserialize, Debug, Clone, Copy)]
@ -41,3 +41,45 @@ pub struct Resource {
pub slots_state: u32, pub slots_state: u32,
pub hours: Vec<HourBlock>, 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)
}
}

View File

@ -1,7 +1,7 @@
use chrono::NaiveTime; use chrono::NaiveTime;
use serde::{self, Deserialize, Deserializer, Serializer}; 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> pub fn serialize<S>(time: &NaiveTime, serializer: S) -> Result<S::Ok, S::Error>
where where

View File

@ -1,24 +1,24 @@
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Data<T> { pub struct Data<T> {
pub data: T, pub data: T,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataCategory { pub struct SiteDataCategory {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
pub name_plural: String, pub name_plural: String,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataLocationCoordinates { pub struct SiteDataLocationCoordinates {
pub latitude: f64, pub latitude: f64,
pub longitude: f64, pub longitude: f64,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataLocationAddress { pub struct SiteDataLocationAddress {
pub route: String, pub route: String,
pub city: String, pub city: String,
@ -27,47 +27,47 @@ pub struct SiteDataLocationAddress {
pub country_code: String, pub country_code: String,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataLocation { pub struct SiteDataLocation {
pub coordinates: SiteDataLocationCoordinates, pub coordinates: SiteDataLocationCoordinates,
pub address: SiteDataLocationAddress, pub address: SiteDataLocationAddress,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataForecast { pub struct SiteDataForecast {
pub opened: bool, pub opened: bool,
pub occupancy: u32, pub occupancy: Option<u32>,
// waiting_time // waiting_time
pub waiting_time_overflow: bool, pub waiting_time_overflow: bool,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataNotice { pub struct SiteDataNotice {
pub message: String, pub message: String,
pub url: Option<String>, pub url: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataService { pub struct SiteDataService {
pub id: u32, pub id: u32,
pub name: String, pub name: String,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataInfo { pub struct SiteDataInfo {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub url: Option<String>, pub url: Option<String>,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteDataStatus { pub struct SiteDataStatus {
pub state: String, pub state: String,
pub text: String, pub text: String,
pub color: String, pub color: String,
} }
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SiteData { pub struct SiteData {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub slug: String, pub slug: String,
@ -104,3 +104,15 @@ pub struct SiteData {
#[serde(rename = "publicationStatus")] #[serde(rename = "publicationStatus")]
pub publication_status: String, 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>,
}

View File

@ -1,5 +1,5 @@
[package] [package]
name = "affy" name = "affluences-cli"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -7,7 +7,6 @@ edition = "2021"
[dependencies] [dependencies]
affluences-api = { path = "../affluences-api" } affluences-api = { path = "../affluences-api" }
clap = { version = "4.2.7", features = ["derive"] }
serde_json = "1.0.96"
tokio = { version = "1.28.1", features = ["full"] } tokio = { version = "1.28.1", features = ["full"] }
chrono = "*"
uuid = "*"
poise = "0.5.5"

View File

@ -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 => {}
}
}

View File

@ -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> {

3
build.rs 100644
View File

@ -0,0 +1,3 @@
fn main() {
println!("cargo:rerun-if-changed=migrations");
}

8
diesel.toml 100644
View File

@ -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"

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE users;

View File

@ -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)
);

165
src/commands/bib.rs 100644
View File

@ -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> {
// }

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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(())
}

43
src/db/mod.rs 100644
View File

@ -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)
}

12
src/db/schema.rs 100644
View File

@ -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,
}
}

79
src/db/users.rs 100644
View File

@ -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)
}
}

View File

@ -1,8 +1,11 @@
mod commands; mod commands;
mod db;
use affluences_api::AffluencesClient; use affluences_api::AffluencesClient;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::sqlite::SqliteConnection;
use poise::serenity_prelude as serenity; 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 // Types used by all command functions
type Error = Box<dyn std::error::Error + Send + Sync>; 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 // Custom user data passed to all command functions
pub struct Data { pub struct Data {
votes: Mutex<HashMap<String, u32>>,
client: AffluencesClient, client: AffluencesClient,
pool: Pool<ConnectionManager<SqliteConnection>>,
} }
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { 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 // FrameworkOptions contains all of poise's configuration option in one struct
// Every option can be omitted to use its default value // Every option can be omitted to use its default value
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands: vec![ commands: commands::commands(),
commands::help(),
commands::vote(),
commands::getvotes(),
commands::available(),
],
prefix_options: poise::PrefixFrameworkOptions { prefix_options: poise::PrefixFrameworkOptions {
prefix: Some("~".into()), prefix: Some("~".into()),
edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))), edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))),
@ -86,6 +84,8 @@ async fn main() {
..Default::default() ..Default::default()
}; };
let pool = db::initialize_pool("affy.db").expect("Failed to initialize database.");
poise::Framework::builder() poise::Framework::builder()
.token( .token(
var("DISCORD_TOKEN") var("DISCORD_TOKEN")
@ -96,8 +96,8 @@ async fn main() {
println!("Logged in as {}", _ready.user.name); println!("Logged in as {}", _ready.user.name);
poise::builtins::register_globally(ctx, &framework.options().commands).await?; poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data { Ok(Data {
votes: Mutex::new(HashMap::new()),
client: AffluencesClient::new(), client: AffluencesClient::new(),
pool,
}) })
}) })
}) })