diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f8afc7e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.woodpecker/ +target/ diff --git a/.env b/.env new file mode 100644 index 0000000..3e6e71e --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite://affy.db diff --git a/.gitignore b/.gitignore index b128699..ac95708 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ rust-project.json # End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer +*.db* diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..5779344 --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,11 @@ +platform: 'linux/amd64' + +branches: + exclude: [main] + +pipeline: + build: + image: 'rust:1.69' + commands: + - cargo build --verbose + - cargo test --verbose diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml new file mode 100644 index 0000000..92851e5 --- /dev/null +++ b/.woodpecker/clippy.yml @@ -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 diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 0000000..1fd44e2 --- /dev/null +++ b/.woodpecker/deploy.yml @@ -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" diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml new file mode 100644 index 0000000..ad2b612 --- /dev/null +++ b/.woodpecker/lint.yml @@ -0,0 +1,11 @@ +platform: 'linux/amd64' + +branches: + exclude: [main] + +pipeline: + lint: + image: 'rust:1.69' + commands: + - rustup component add rustfmt + - cargo fmt -- --check diff --git a/Cargo.lock b/Cargo.lock index afa86eb..1237a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index e07b19a..518d125 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b2cbacc --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/affluences-api/API.md b/affluences-api/API.md index fc0ecd0..708641e 100644 --- a/affluences-api/API.md +++ b/affluences-api/API.md @@ -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>` + +### 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 diff --git a/affluences-api/src/lib.rs b/affluences-api/src/lib.rs index 17c7ace..7994b0d 100644 --- a/affluences-api/src/lib.rs +++ b/affluences-api/src/lib.rs @@ -20,6 +20,23 @@ impl AffluencesClient { } } + pub async fn search(&self, query: String) -> reqwest::Result { + 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::>() + .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() + } +} diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index 4b63f5f..e10052c 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -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, } + +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) + } +} diff --git a/affluences-api/src/models/hh_mm_time_format.rs b/affluences-api/src/models/hh_mm_time_format.rs index 7d535f6..8858d4f 100644 --- a/affluences-api/src/models/hh_mm_time_format.rs +++ b/affluences-api/src/models/hh_mm_time_format.rs @@ -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(time: &NaiveTime, serializer: S) -> Result where diff --git a/affluences-api/src/models/site_data.rs b/affluences-api/src/models/site_data.rs index 1d13a22..68f5efb 100644 --- a/affluences-api/src/models/site_data.rs +++ b/affluences-api/src/models/site_data.rs @@ -1,24 +1,24 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct Data { 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, // waiting_time pub waiting_time_overflow: bool, } -#[derive(Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct SiteDataNotice { pub message: String, pub url: Option, } -#[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, } -#[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, +} diff --git a/bot/Cargo.toml b/affluences-cli/Cargo.toml similarity index 72% rename from bot/Cargo.toml rename to affluences-cli/Cargo.toml index 41a372d..f46a340 100644 --- a/bot/Cargo.toml +++ b/affluences-cli/Cargo.toml @@ -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" diff --git a/affluences-cli/src/main.rs b/affluences-cli/src/main.rs new file mode 100644 index 0000000..3b31cb5 --- /dev/null +++ b/affluences-cli/src/main.rs @@ -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, +} + +#[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 => {} + } +} diff --git a/bot/src/commands.rs b/bot/src/commands.rs deleted file mode 100644 index 60d82d5..0000000 --- a/bot/src/commands.rs +++ /dev/null @@ -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, -) -> 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, -) -> 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 = 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> { diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..3a8149e --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..88db6ba --- /dev/null +++ b/diesel.toml @@ -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" diff --git a/migrations/2023-05-15-142901_create_users/down.sql b/migrations/2023-05-15-142901_create_users/down.sql new file mode 100644 index 0000000..dc3714b --- /dev/null +++ b/migrations/2023-05-15-142901_create_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users; diff --git a/migrations/2023-05-15-142901_create_users/up.sql b/migrations/2023-05-15-142901_create_users/up.sql new file mode 100644 index 0000000..b1ec088 --- /dev/null +++ b/migrations/2023-05-15-142901_create_users/up.sql @@ -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) +); diff --git a/src/commands/bib.rs b/src/commands/bib.rs new file mode 100644 index 0000000..32fbbe2 --- /dev/null +++ b/src/commands/bib.rs @@ -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::>() + .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::::into(date) + )) + .fields( + resources + .into_iter() + .map(resource_to_embed_field) + .collect::>(), + ) + }) + }) + .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, +) -> 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::::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> { + +// } diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs new file mode 100644 index 0000000..b6250d4 --- /dev/null +++ b/src/commands/minecraft.rs @@ -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, + #[description = "Port the server runs on"] port: Option, +) -> 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(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..32551d4 --- /dev/null +++ b/src/commands/mod.rs @@ -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 { + 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::() { + 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 for chrono::NaiveDate { + fn from(val: HumanNaiveDate) -> chrono::NaiveDate { + val.0 + } +} + +pub fn commands() -> Vec> { + 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, +) -> 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(()) +} diff --git a/src/commands/users.rs b/src/commands/users.rs new file mode 100644 index 0000000..f552787 --- /dev/null +++ b/src/commands/users.rs @@ -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(()) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..578506e --- /dev/null +++ b/src/db/mod.rs @@ -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; + +fn run_migrations(connection: &mut impl MigrationHarness) -> 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>, 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) +} diff --git a/src/db/schema.rs b/src/db/schema.rs new file mode 100644 index 0000000..e43eb3e --- /dev/null +++ b/src/db/schema.rs @@ -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, + } +} diff --git a/src/db/users.rs b/src/db/users.rs new file mode 100644 index 0000000..e87f0b9 --- /dev/null +++ b/src/db/users.rs @@ -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>; +type WithGuild = Eq; +type ByGuild = Filter>; + +impl User { + pub fn all() -> All { + users::table.select(User::as_select()) + } + + // pub fn by_guild(guild_id_: T) -> ByGuild + // where T: AsExpression + // { + // Self::all().filter(guild_id.eq(guild_id_)) + // } + + pub fn by_guild_id(guild_id_: i64) -> ByGuild { + Self::all().filter(guild_id.eq(guild_id_)) + } + + pub fn get( + conn: &mut SqliteConnection, + guild_id_: i64, + discord_id_: i64, + ) -> Result, 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, diesel::result::Error> { + Self::all().find(id_).first(conn).optional() + } + + pub fn update(&self, conn: &mut SqliteConnection) -> Result { + diesel::update(users::table).set(self).execute(conn) + } +} + +impl NewUser { + pub fn insert(&self, conn: &mut SqliteConnection) -> Result { + diesel::insert_into(users::table) + .values(self) + .get_result(conn) + } +} diff --git a/bot/src/main.rs b/src/main.rs similarity index 91% rename from bot/src/main.rs rename to src/main.rs index f6e06bb..6328584 100644 --- a/bot/src/main.rs +++ b/src/main.rs @@ -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; @@ -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>, client: AffluencesClient, + pool: Pool>, } 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, }) }) })