From 3126f812004cebe67799d0177df82b55fd1554d6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 May 2023 10:12:25 +0200 Subject: [PATCH 01/25] chore: add ci config --- .woodpecker/build.yml | 10 ++++++++++ .woodpecker/clippy.yml | 10 ++++++++++ .woodpecker/lint.yml | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 .woodpecker/build.yml create mode 100644 .woodpecker/clippy.yml create mode 100644 .woodpecker/lint.yml diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..f792be9 --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,10 @@ +platform: 'linux/amd64' + +branches: + exclude: [main] + +pipeline: + build: + image: 'rust:1.69' + commands: + - cargo build diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml new file mode 100644 index 0000000..c100904 --- /dev/null +++ b/.woodpecker/clippy.yml @@ -0,0 +1,10 @@ +platform: 'linux/amd64' + +branches: + exclude: [main] + +pipeline: + clippy: + image: 'rust:1.69' + commands: + - cargo clippy -- --no-deps -Dwarnings diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml new file mode 100644 index 0000000..01065f8 --- /dev/null +++ b/.woodpecker/lint.yml @@ -0,0 +1,10 @@ +platform: 'linux/amd64' + +branches: + exclude: [main] + +pipeline: + lint: + image: 'rust:1.69' + commands: + - cargo fmt -- --check From 8909ac57a8c0c02ce0b6bfabf5f47cafc955d464 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 May 2023 12:29:35 +0200 Subject: [PATCH 02/25] docs: start of api reference file --- affluences-api/API.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) 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 From 29f13d49b94b81e55f8e95496b31587145e452a6 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 May 2023 13:08:03 +0200 Subject: [PATCH 03/25] feat: start affluences cli for testing --- Cargo.lock | 191 ++++++++++++++++++++++++- Cargo.toml | 1 + affluences-api/src/lib.rs | 9 ++ affluences-api/src/models/site_data.rs | 38 +++-- affluences-cli/Cargo.toml | 12 ++ affluences-cli/src/main.rs | 30 ++++ bot/src/commands.rs | 2 + 7 files changed, 269 insertions(+), 14 deletions(-) create mode 100644 affluences-cli/Cargo.toml create mode 100644 affluences-cli/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index afa86eb..5d0a985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "affluences-cli" +version = "0.1.0" +dependencies = [ + "affluences-api", + "clap", + "serde_json", + "tokio", +] + [[package]] name = "affy" version = "0.1.0" @@ -38,6 +48,55 @@ 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-trait" version = "0.1.68" @@ -144,6 +203,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 +255,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" @@ -311,6 +418,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 +587,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 +602,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 +729,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" @@ -625,6 +788,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" @@ -712,7 +881,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", ] @@ -938,6 +1107,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" @@ -1432,6 +1615,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" diff --git a/Cargo.toml b/Cargo.toml index e07b19a..eea9b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,6 @@ members = [ "affluences-api", + "affluences-cli", "bot", ] diff --git a/affluences-api/src/lib.rs b/affluences-api/src/lib.rs index 17c7ace..3e11326 100644 --- a/affluences-api/src/lib.rs +++ b/affluences-api/src/lib.rs @@ -20,6 +20,15 @@ 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, diff --git a/affluences-api/src/models/site_data.rs b/affluences-api/src/models/site_data.rs index 1d13a22..c2a7d98 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::{Serialize, Deserialize}; -#[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/affluences-cli/Cargo.toml b/affluences-cli/Cargo.toml new file mode 100644 index 0000000..f46a340 --- /dev/null +++ b/affluences-cli/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "affluences-cli" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[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"] } 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 index 60d82d5..6a2c548 100644 --- a/bot/src/commands.rs +++ b/bot/src/commands.rs @@ -158,3 +158,5 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { // ctx: Context<'_>, // date: NaiveDate, // ) -> Result<(), Error> { + +// } From fd81b6426266319248ee5f01164296947f1247f1 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 13 May 2023 13:10:24 +0200 Subject: [PATCH 04/25] fix(ci): add clippy and rustfmt --- .woodpecker/clippy.yml | 1 + .woodpecker/lint.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml index c100904..92851e5 100644 --- a/.woodpecker/clippy.yml +++ b/.woodpecker/clippy.yml @@ -7,4 +7,5 @@ pipeline: clippy: image: 'rust:1.69' commands: + - rustup component add clippy - cargo clippy -- --no-deps -Dwarnings diff --git a/.woodpecker/lint.yml b/.woodpecker/lint.yml index 01065f8..ad2b612 100644 --- a/.woodpecker/lint.yml +++ b/.woodpecker/lint.yml @@ -7,4 +7,5 @@ pipeline: lint: image: 'rust:1.69' commands: + - rustup component add rustfmt - cargo fmt -- --check From f9d31ce114316beb838fb7a47763e7a594ff73af Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 14 May 2023 11:43:04 +0200 Subject: [PATCH 05/25] refactor: make affy main cargo project --- Cargo.toml | 13 ++++++++++++- bot/Cargo.toml | 13 ------------- {bot/src => src}/commands.rs | 0 {bot/src => src}/main.rs | 0 4 files changed, 12 insertions(+), 14 deletions(-) delete mode 100644 bot/Cargo.toml rename {bot/src => src}/commands.rs (100%) rename {bot/src => src}/main.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index eea9b1e..f52117f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,16 @@ members = [ "affluences-api", "affluences-cli", - "bot", ] + +[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" diff --git a/bot/Cargo.toml b/bot/Cargo.toml deleted file mode 100644 index 41a372d..0000000 --- a/bot/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "affy" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -affluences-api = { path = "../affluences-api" } -tokio = { version = "1.28.1", features = ["full"] } -chrono = "*" -uuid = "*" -poise = "0.5.5" diff --git a/bot/src/commands.rs b/src/commands.rs similarity index 100% rename from bot/src/commands.rs rename to src/commands.rs diff --git a/bot/src/main.rs b/src/main.rs similarity index 100% rename from bot/src/main.rs rename to src/main.rs From 60252e5f421a7217e48c0e1ab6731de4ac7bd74a Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sun, 14 May 2023 11:45:50 +0200 Subject: [PATCH 06/25] chore: don't build cli tool by default --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f52117f..77a1561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,10 @@ members = [ "affluences-api", "affluences-cli", ] +# Don't build the CLI tool by default +default-members = [ + "affluences-api", +] [package] name = "affy" From 21742a231758f7aab0ba2c00cfc9affa23297334 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 14 May 2023 22:42:57 +0200 Subject: [PATCH 07/25] fix: add root project in default-members --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 77a1561..d77de7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ ] # Don't build the CLI tool by default default-members = [ + ".", "affluences-api", ] From 3fbec90bb44c41cfeadd26c9386805cccc6d0001 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 14 May 2023 23:36:29 +0200 Subject: [PATCH 08/25] chore: add Dockerfile --- .dockerignore | 2 ++ Dockerfile | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..130951a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +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 + +USER www-data:www-data + +ENTRYPOINT ["/bin/dumb-init", "--"] +CMD ["/bin/affy"] From a74cf76b2b20444d3c503894f8c6ab7a5d901670 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 14 May 2023 23:38:30 +0200 Subject: [PATCH 09/25] chore: please clippy and rustfmt --- affluences-api/src/lib.rs | 20 ++++++++++++++++--- .../src/models/hh_mm_time_format.rs | 2 +- affluences-api/src/models/site_data.rs | 6 +++--- src/commands.rs | 6 +++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/affluences-api/src/lib.rs b/affluences-api/src/lib.rs index 3e11326..7994b0d 100644 --- a/affluences-api/src/lib.rs +++ b/affluences-api/src/lib.rs @@ -22,11 +22,19 @@ 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 + let body = SiteSearch { + search_query: query, }; - Ok(self.client.post(url).json(&body).send().await?.json::>().await?.data) + Ok(self + .client + .post(url) + .json(&body) + .send() + .await? + .json::>() + .await? + .data) } pub async fn available( @@ -81,3 +89,9 @@ impl AffluencesClient { .await } } + +impl Default for AffluencesClient { + fn default() -> Self { + AffluencesClient::new() + } +} 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 c2a7d98..68f5efb 100644 --- a/affluences-api/src/models/site_data.rs +++ b/affluences-api/src/models/site_data.rs @@ -1,4 +1,4 @@ -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] pub struct Data { @@ -107,12 +107,12 @@ pub struct SiteData { #[derive(Serialize, Debug)] pub struct SiteSearch { - pub search_query: String + pub search_query: String, } #[derive(Serialize, Deserialize, Debug)] pub struct SiteSearchResponse { pub page: u32, pub max_size: u32, - pub results: Vec + pub results: Vec, } diff --git a/src/commands.rs b/src/commands.rs index 6a2c548..b955679 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,7 +4,7 @@ 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"; +const TIME_FORMAT: &str = "%H:%M"; /// Show this help menu #[poise::command(prefix_command, track_edits, slash_command)] @@ -90,7 +90,7 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { let mut fields: Vec<(String, String, bool)> = Default::default(); for resource in &resources { - if resource.hours.len() == 0 { + if resource.hours.is_empty() { fields.push(( resource.resource_name.clone(), "Nothing available.".to_string(), @@ -158,5 +158,5 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { // ctx: Context<'_>, // date: NaiveDate, // ) -> Result<(), Error> { - + // } From 2999ca23014d6150252d26aebb01a2d89b575aa8 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 15 May 2023 11:19:07 +0200 Subject: [PATCH 10/25] refactor: clean up some code --- .woodpecker/build.yml | 3 +- affluences-api/src/models/available.rs | 38 ++++++++- src/commands.rs | 113 ++++--------------------- src/main.rs | 7 +- 4 files changed, 58 insertions(+), 103 deletions(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index f792be9..5779344 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -7,4 +7,5 @@ pipeline: build: image: 'rust:1.69' commands: - - cargo build + - cargo build --verbose + - cargo test --verbose diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index 4b63f5f..dd0980a 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,39 @@ pub struct Resource { pub slots_state: u32, pub hours: Vec, } + +impl Resource { + pub fn condensed_hours(&self) -> Vec<(&HourBlock, Duration)> { + let mut start_hour_opt: Option<&HourBlock> = None; + let mut duration = Duration::seconds(0); + let mut out: Vec<(&HourBlock, Duration)> = Default::default(); + + for hour in &self.hours { + if let Some(start_hour) = start_hour_opt { + if hour.state == start_hour.state { + duration = duration + Duration::minutes(hour.granularity.into()); + } else { + out.push((start_hour, duration)); + start_hour_opt = Some(hour); + duration = Duration::minutes(hour.granularity.into()); + } + } else if hour.state == 1 { + start_hour_opt = Some(hour); + duration = Duration::minutes(hour.granularity.into()); + } + } + + if let Some(start_hour) = start_hour_opt { + 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() + } +} diff --git a/src/commands.rs b/src/commands.rs index b955679..dc74e0c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,5 @@ use crate::{Context, Error}; -use affluences_api::HourBlock; -use chrono::{Duration, NaiveDate}; +use chrono::NaiveDate; use uuid::{uuid, Uuid}; const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); @@ -26,62 +25,6 @@ pub async fn help( 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> { @@ -90,7 +33,9 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { let mut fields: Vec<(String, String, bool)> = Default::default(); for resource in &resources { - if resource.hours.is_empty() { + let available_hours = resource.condensed_available_hours(); + + if available_hours.is_empty() { fields.push(( resource.resource_name.clone(), "Nothing available.".to_string(), @@ -100,45 +45,23 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { 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!( + fields.push(( + resource.resource_name.clone(), + available_hours + .into_iter() + .map(|(start_block, duration)| { + format!( "{} - {} ({:02}:{:02})", - start_hour.hour.format(TIME_FORMAT), - end_hour.format(TIME_FORMAT), + start_block.hour.format(TIME_FORMAT), + (start_block.hour + duration).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)); + ) + }) + .collect::>() + .join("\n"), + false, + )); } ctx.send(|f| { diff --git a/src/main.rs b/src/main.rs index f6e06bb..b18e13e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,12 +36,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: vec![commands::help(), commands::available()], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))), From 38994a29a0443ec07b50706438218d0ae7aee5ec Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 15 May 2023 13:52:30 +0200 Subject: [PATCH 11/25] refactor: move commands to separate module --- Cargo.lock | 14 +++++++++++ Cargo.toml | 1 + Dockerfile | 2 ++ src/{commands.rs => commands/affluence.rs} | 21 +---------------- src/commands/mod.rs | 27 ++++++++++++++++++++++ src/main.rs | 6 ++--- 6 files changed, 47 insertions(+), 24 deletions(-) rename src/{commands.rs => commands/affluence.rs} (75%) create mode 100644 src/commands/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5d0a985..99099f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,7 @@ name = "affy" version = "0.1.0" dependencies = [ "affluences-api", + "async-minecraft-ping", "chrono", "poise", "tokio", @@ -97,6 +98,19 @@ dependencies = [ "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" diff --git a/Cargo.toml b/Cargo.toml index d77de7b..0a82e38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ tokio = { version = "1.28.1", features = ["full"] } chrono = "*" uuid = "*" poise = "0.5.5" +async-minecraft-ping = "0.8.0" diff --git a/Dockerfile b/Dockerfile index 130951a..b2cbacc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,8 @@ 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", "--"] diff --git a/src/commands.rs b/src/commands/affluence.rs similarity index 75% rename from src/commands.rs rename to src/commands/affluence.rs index dc74e0c..60b8730 100644 --- a/src/commands.rs +++ b/src/commands/affluence.rs @@ -1,30 +1,11 @@ use crate::{Context, Error}; + use chrono::NaiveDate; use uuid::{uuid, Uuid}; const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); const TIME_FORMAT: &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(()) -} - /// List available timeslots for day #[poise::command(prefix_command, slash_command)] pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..bfb7034 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,27 @@ +mod affluence; + +use crate::{Context, Data, Error}; + +pub fn commands() -> Vec> { + vec![help(), affluence::available()] +} + +/// 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(()) +} diff --git a/src/main.rs b/src/main.rs index b18e13e..c052b07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod commands; use affluences_api::AffluencesClient; 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,7 +10,6 @@ type Context<'a> = poise::Context<'a, Data, Error>; // Custom user data passed to all command functions pub struct Data { - votes: Mutex>, client: AffluencesClient, } @@ -36,7 +35,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::available()], + commands: commands::commands(), prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))), @@ -91,7 +90,6 @@ 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(), }) }) From c966529a2b6fb0b451922a317089b8c747f05372 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 15 May 2023 14:24:31 +0200 Subject: [PATCH 12/25] feat: add minecraft ping command --- src/commands/minecraft.rs | 47 +++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 3 ++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/commands/minecraft.rs diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs new file mode 100644 index 0000000..fbbedd9 --- /dev/null +++ b/src/commands/minecraft.rs @@ -0,0 +1,47 @@ +use crate::{Context, Error}; +use async_minecraft_ping::ServerDescription; + +const DEFAULT_SERVER: &str = "rustybever.be"; + +/// Ping a minecraft server +#[poise::command(prefix_command, slash_command)] +pub async fn ping_mc( + 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 index bfb7034..24ca959 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,9 +1,10 @@ mod affluence; +mod minecraft; use crate::{Context, Data, Error}; pub fn commands() -> Vec> { - vec![help(), affluence::available()] + vec![help(), affluence::available(), minecraft::ping_mc()] } /// Show this help menu From f8ce315d8e2e1f594a355bc48f23e391eec7d5ac Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 15 May 2023 15:22:43 +0200 Subject: [PATCH 13/25] refactor: this is so fun --- affluences-api/src/models/available.rs | 29 +++++++--------- src/commands/affluence.rs | 48 ++++++++++++++------------ src/commands/mod.rs | 2 ++ 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index dd0980a..29df5c5 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -44,28 +44,25 @@ pub struct Resource { impl Resource { pub fn condensed_hours(&self) -> Vec<(&HourBlock, Duration)> { - let mut start_hour_opt: Option<&HourBlock> = None; - let mut duration = Duration::seconds(0); + 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 { - if let Some(start_hour) = start_hour_opt { - if hour.state == start_hour.state { - duration = duration + Duration::minutes(hour.granularity.into()); - } else { - out.push((start_hour, duration)); - start_hour_opt = Some(hour); - duration = Duration::minutes(hour.granularity.into()); - } - } else if hour.state == 1 { - start_hour_opt = Some(hour); + 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()); } } - if let Some(start_hour) = start_hour_opt { - out.push((start_hour, duration)); - } + out.push((start_hour, duration)); out } diff --git a/src/commands/affluence.rs b/src/commands/affluence.rs index 60b8730..47d5f9b 100644 --- a/src/commands/affluence.rs +++ b/src/commands/affluence.rs @@ -1,32 +1,24 @@ +use crate::commands::EmbedField; use crate::{Context, Error}; +use affluences_api::Resource; use chrono::NaiveDate; use uuid::{uuid, Uuid}; const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); const TIME_FORMAT: &str = "%H:%M"; -/// 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(); +fn resource_to_embed_field(resource: Resource) -> EmbedField { + let available_hours = resource.condensed_available_hours(); - for resource in &resources { - let available_hours = resource.condensed_available_hours(); - - if available_hours.is_empty() { - fields.push(( - resource.resource_name.clone(), - "Nothing available.".to_string(), - false, - )); - - continue; - } - - fields.push(( + if available_hours.is_empty() { + ( + resource.resource_name.clone(), + "Nothing available.".to_string(), + false, + ) + } else { + ( resource.resource_name.clone(), available_hours .into_iter() @@ -42,13 +34,25 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { .collect::>() .join("\n"), false, - )); + ) } +} + +/// 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?; ctx.send(|f| { f.embed(|e| { e.description(format!("Available booking dates for {}.", date)) - .fields(fields) + .fields( + resources + .into_iter() + .map(resource_to_embed_field) + .collect::>(), + ) }) }) .await?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 24ca959..70a5dea 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,6 +3,8 @@ mod minecraft; use crate::{Context, Data, Error}; +type EmbedField = (String, String, bool); + pub fn commands() -> Vec> { vec![help(), affluence::available(), minecraft::ping_mc()] } From e834b3308ad21d8c437e10f7d536381de5f85ba1 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Mon, 15 May 2023 17:38:13 +0200 Subject: [PATCH 14/25] feat: initial register functionality --- .env | 1 + .gitignore | 1 + Cargo.lock | 69 +++++++++++++++++++ Cargo.toml | 1 + diesel.toml | 8 +++ migrations/.keep | 0 .../2023-05-15-142901_create_users/down.sql | 2 + .../2023-05-15-142901_create_users/up.sql | 7 ++ src/commands/mod.rs | 9 ++- src/commands/users.rs | 45 ++++++++++++ src/db/mod.rs | 2 + src/db/schema.rs | 10 +++ src/db/users.rs | 22 ++++++ src/main.rs | 7 ++ src/models.rs | 0 15 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 .env create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/2023-05-15-142901_create_users/down.sql create mode 100644 migrations/2023-05-15-142901_create_users/up.sql create mode 100644 src/commands/users.rs create mode 100644 src/db/mod.rs create mode 100644 src/db/schema.rs create mode 100644 src/db/users.rs create mode 100644 src/models.rs 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..a7e99c9 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/Cargo.lock b/Cargo.lock index 99099f4..c88ad20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,7 @@ dependencies = [ "affluences-api", "async-minecraft-ping", "chrono", + "diesel", "poise", "tokio", "uuid", @@ -413,6 +414,28 @@ 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", +] + +[[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 = "digest" version = "0.10.6" @@ -793,6 +816,16 @@ 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 = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -955,6 +988,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" @@ -992,6 +1031,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" @@ -1644,6 +1707,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 0a82e38..a4d6e6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ 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"] } 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/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 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..e5148b3 --- /dev/null +++ b/migrations/2023-05-15-142901_create_users/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE users ( + discord_id UNSIGNED BIG INT PRIMARY KEY NOT NULL, + email TEXT UNIQUE NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL +); diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 70a5dea..d0f47ce 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,12 +1,19 @@ mod affluence; mod minecraft; +mod users; use crate::{Context, Data, Error}; type EmbedField = (String, String, bool); pub fn commands() -> Vec> { - vec![help(), affluence::available(), minecraft::ping_mc()] + vec![ + help(), + affluence::available(), + minecraft::ping_mc(), + users::register(), + users::registered(), + ] } /// Show this help menu diff --git a/src/commands/users.rs b/src/commands/users.rs new file mode 100644 index 0000000..df0e7e6 --- /dev/null +++ b/src/commands/users.rs @@ -0,0 +1,45 @@ +use crate::db::users::{user_all, user_insert, User}; +use crate::{Context, Error}; + +#[poise::command(prefix_command, slash_command)] +pub async fn register( + ctx: Context<'_>, + first_name: String, + last_name: String, + email: String, +) -> Result<(), Error> { + let user = User { + discord_id: ctx.author().id.0 as i64, + first_name, + last_name, + email, + }; + + { + let mut conn = ctx.data().conn.lock().unwrap(); + user_insert(&mut conn, &user); + } + + Ok(()) +} + +#[poise::command(prefix_command, slash_command)] +pub async fn registered(ctx: Context<'_>) -> Result<(), Error> { + let users = { + let mut conn = ctx.data().conn.lock().unwrap(); + user_all(&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?; + + Ok(()) +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..aa8f28e --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,2 @@ +mod schema; +pub mod users; diff --git a/src/db/schema.rs b/src/db/schema.rs new file mode 100644 index 0000000..f50726b --- /dev/null +++ b/src/db/schema.rs @@ -0,0 +1,10 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + users (discord_id) { + discord_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..17040b0 --- /dev/null +++ b/src/db/users.rs @@ -0,0 +1,22 @@ +use super::schema::users; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; + +#[derive(Queryable, Insertable)] +pub struct User { + pub discord_id: i64, + pub email: String, + pub first_name: String, + pub last_name: String, +} + +pub fn user_insert(conn: &mut SqliteConnection, user: &User) -> User { + diesel::insert_into(users::table) + .values(user) + .get_result(conn) + .expect("fuck") +} + +pub fn user_all(conn: &mut SqliteConnection) -> Vec { + users::table.load::(conn).expect("nou") +} diff --git a/src/main.rs b/src/main.rs index c052b07..e4364d8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ mod commands; +mod db; use affluences_api::AffluencesClient; +use diesel::Connection; use poise::serenity_prelude as serenity; +use std::sync::Mutex; use std::{env::var, time::Duration}; // Types used by all command functions @@ -11,6 +14,7 @@ type Context<'a> = poise::Context<'a, Data, Error>; // Custom user data passed to all command functions pub struct Data { client: AffluencesClient, + conn: Mutex, } async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { @@ -91,6 +95,9 @@ async fn main() { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { client: AffluencesClient::new(), + conn: Mutex::new( + diesel::sqlite::SqliteConnection::establish("affy.db").unwrap(), + ), }) }) }) diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..e69de29 From 303e3ffd4e31e7bad88006a14964504b151ee234 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 16 May 2023 09:00:12 +0200 Subject: [PATCH 15/25] feat: add database connection pooling --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 2 +- src/commands/users.rs | 4 ++-- src/main.rs | 14 ++++++++++---- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c88ad20..c9d2c0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,7 @@ checksum = "72eb77396836a4505da85bae0712fa324b74acfe1876d7c2f7e694ef3d0ee373" dependencies = [ "diesel_derives", "libsqlite3-sys", + "r2d2", ] [[package]] @@ -1073,6 +1074,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" @@ -1231,6 +1243,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" diff --git a/Cargo.toml b/Cargo.toml index a4d6e6f..1faac2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,4 @@ 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"] } +diesel = { version = "2.0.4", features = ["sqlite", "returning_clauses_for_sqlite_3_35", "r2d2"] } diff --git a/src/commands/users.rs b/src/commands/users.rs index df0e7e6..e164d9a 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -16,7 +16,7 @@ pub async fn register( }; { - let mut conn = ctx.data().conn.lock().unwrap(); + let mut conn = ctx.data().pool.get()?; user_insert(&mut conn, &user); } @@ -26,7 +26,7 @@ pub async fn register( #[poise::command(prefix_command, slash_command)] pub async fn registered(ctx: Context<'_>) -> Result<(), Error> { let users = { - let mut conn = ctx.data().conn.lock().unwrap(); + let mut conn = ctx.data().pool.get()?; user_all(&mut conn) }; diff --git a/src/main.rs b/src/main.rs index e4364d8..2bc1ab9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,8 @@ use diesel::Connection; use poise::serenity_prelude as serenity; use std::sync::Mutex; use std::{env::var, time::Duration}; +use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::sqlite::SqliteConnection; // Types used by all command functions type Error = Box; @@ -14,7 +16,7 @@ type Context<'a> = poise::Context<'a, Data, Error>; // Custom user data passed to all command functions pub struct Data { client: AffluencesClient, - conn: Mutex, + pool: Pool> } async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { @@ -34,6 +36,12 @@ async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { } } +fn db_connection_pool(url: &str) -> Pool> { + let manager = ConnectionManager::new(url); + + Pool::builder().test_on_check_out(true).build(manager).expect("oops") +} + #[tokio::main] async fn main() { // FrameworkOptions contains all of poise's configuration option in one struct @@ -95,9 +103,7 @@ async fn main() { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { client: AffluencesClient::new(), - conn: Mutex::new( - diesel::sqlite::SqliteConnection::establish("affy.db").unwrap(), - ), + pool: db_connection_pool("affy.db") }) }) }) From 0b10885015ba820f7655ad49d07c7a23d9c8c14e Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 16 May 2023 15:47:13 +0200 Subject: [PATCH 16/25] feat: starting db abstractions --- .../2023-05-15-142901_create_users/up.sql | 8 ++- src/commands/users.rs | 56 +++++++++------- src/db/mod.rs | 21 ++++++ src/db/schema.rs | 4 +- src/db/users.rs | 64 ++++++++++++++++--- src/main.rs | 10 +-- 6 files changed, 119 insertions(+), 44 deletions(-) diff --git a/migrations/2023-05-15-142901_create_users/up.sql b/migrations/2023-05-15-142901_create_users/up.sql index e5148b3..b1ec088 100644 --- a/migrations/2023-05-15-142901_create_users/up.sql +++ b/migrations/2023-05-15-142901_create_users/up.sql @@ -1,7 +1,11 @@ -- Your SQL goes here CREATE TABLE users ( - discord_id UNSIGNED BIG INT PRIMARY KEY NOT NULL, + 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 + last_name TEXT NOT NULL, + + UNIQUE(discord_id, guild_id) ); diff --git a/src/commands/users.rs b/src/commands/users.rs index e164d9a..e692b6a 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -1,5 +1,6 @@ -use crate::db::users::{user_all, user_insert, User}; +use crate::db::users::{User, NewUser}; use crate::{Context, Error}; +use diesel::RunQueryDsl; #[poise::command(prefix_command, slash_command)] pub async fn register( @@ -8,16 +9,21 @@ pub async fn register( last_name: String, email: String, ) -> Result<(), Error> { - let user = User { - discord_id: ctx.author().id.0 as i64, - first_name, - last_name, - email, - }; + if let Some(guild_id) = ctx.guild_id() { + let new_user = NewUser { + discord_id: ctx.author().id.0 as i64, + guild_id: guild_id.into(), + first_name, + last_name, + email, + }; - { - let mut conn = ctx.data().pool.get()?; - user_insert(&mut conn, &user); + { + let mut conn = ctx.data().pool.get()?; + new_user.insert(&mut conn); + } + } else { + ctx.say("You have to send this message from a guild.").await?; } Ok(()) @@ -25,21 +31,25 @@ pub async fn register( #[poise::command(prefix_command, slash_command)] pub async fn registered(ctx: Context<'_>) -> Result<(), Error> { - let users = { - let mut conn = ctx.data().pool.get()?; - user_all(&mut conn) - }; + 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)), - ) + 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?; + .await?; + } else { + ctx.say("You are not in a guild.").await?; + } Ok(()) } diff --git a/src/db/mod.rs b/src/db/mod.rs index aa8f28e..6c29d3a 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,2 +1,23 @@ mod schema; pub mod users; + +use diesel::sqlite::SqliteConnection; +use diesel::connection::SimpleConnection; +use diesel::QueryResult; +use diesel::r2d2::{ConnectionManager, Pool}; + +fn initialize_db(conn: &mut SqliteConnection) -> QueryResult<()> { + // Enable WAL mode and enforce foreign keys + conn.batch_execute("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;") +} + +pub fn initialize_pool(url: &str) -> Pool> { + let manager = ConnectionManager::new(url); + + let pool = Pool::builder().test_on_check_out(true).build(manager).expect("oops"); + + let mut conn = pool.get().unwrap(); + initialize_db(&mut conn).unwrap(); + + pool +} diff --git a/src/db/schema.rs b/src/db/schema.rs index f50726b..e43eb3e 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -1,8 +1,10 @@ // @generated automatically by Diesel CLI. diesel::table! { - users (discord_id) { + 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 index 17040b0..357ff3b 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,22 +1,68 @@ -use super::schema::users; +use super::schema::users::{self, dsl::*}; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use diesel::dsl::{AsSelect, Select}; +use diesel::sqlite::Sqlite; +use diesel::dsl::Eq; +use diesel::helper_types::Filter; +use diesel::sql_types::BigInt; +use diesel::expression::AsExpression; -#[derive(Queryable, Insertable)] +#[derive(Queryable, Selectable)] +#[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, } -pub fn user_insert(conn: &mut SqliteConnection, user: &User) -> User { - diesel::insert_into(users::table) - .values(user) - .get_result(conn) - .expect("fuck") +#[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, } -pub fn user_all(conn: &mut SqliteConnection) -> Vec { - users::table.load::(conn).expect("nou") +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 { + Self::all().filter(guild_id.eq(guild_id_)).filter(discord_id.eq(discord_id_)).first(conn) + } + + pub fn get_by_id(conn: &mut SqliteConnection, id_: i32) -> Result { + Self::all().filter(id.eq(id_)).first(conn) + } +} + +impl NewUser { + pub fn insert(&self, conn: &mut SqliteConnection) -> User { + diesel::insert_into(users::table) + .values(self) + .get_result(conn) + .expect("fuck") + } } diff --git a/src/main.rs b/src/main.rs index 2bc1ab9..c0a9ae7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,7 @@ mod commands; mod db; use affluences_api::AffluencesClient; -use diesel::Connection; use poise::serenity_prelude as serenity; -use std::sync::Mutex; use std::{env::var, time::Duration}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::sqlite::SqliteConnection; @@ -36,12 +34,6 @@ async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { } } -fn db_connection_pool(url: &str) -> Pool> { - let manager = ConnectionManager::new(url); - - Pool::builder().test_on_check_out(true).build(manager).expect("oops") -} - #[tokio::main] async fn main() { // FrameworkOptions contains all of poise's configuration option in one struct @@ -103,7 +95,7 @@ async fn main() { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { client: AffluencesClient::new(), - pool: db_connection_pool("affy.db") + pool: db::initialize_pool("affy.db") }) }) }) From 04e268a17cfc481e5d1f729814dfb21f90864c21 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 16 May 2023 16:25:33 +0200 Subject: [PATCH 17/25] feat: database migrations --- .gitignore | 2 +- Cargo.lock | 42 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + migrations/.keep | 0 src/build.rs | 3 +++ src/commands/users.rs | 7 +++++-- src/db/mod.rs | 43 +++++++++++++++++++++++++++++++++---------- src/db/users.rs | 28 +++++++++++++++++----------- src/main.rs | 10 ++++++---- 9 files changed, 108 insertions(+), 28 deletions(-) delete mode 100644 migrations/.keep create mode 100644 src/build.rs diff --git a/.gitignore b/.gitignore index a7e99c9..ac95708 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ rust-project.json # End of https://www.toptal.com/developers/gitignore/api/rust,rust-analyzer -*.db +*.db* diff --git a/Cargo.lock b/Cargo.lock index c9d2c0a..b8388a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,7 @@ dependencies = [ "async-minecraft-ping", "chrono", "diesel", + "diesel_migrations", "poise", "tokio", "uuid", @@ -437,6 +438,17 @@ dependencies = [ "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" @@ -867,6 +879,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" @@ -1575,6 +1608,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" diff --git a/Cargo.toml b/Cargo.toml index 1faac2a..cde15dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ 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" ] } diff --git a/migrations/.keep b/migrations/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..508b53c --- /dev/null +++ b/src/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/src/commands/users.rs b/src/commands/users.rs index e692b6a..da968f6 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -1,4 +1,4 @@ -use crate::db::users::{User, NewUser}; +use crate::db::users::{NewUser, User}; use crate::{Context, Error}; use diesel::RunQueryDsl; @@ -22,8 +22,11 @@ pub async fn register( let mut conn = ctx.data().pool.get()?; 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?; + ctx.say("You have to send this message from a guild.") + .await?; } Ok(()) diff --git a/src/db/mod.rs b/src/db/mod.rs index 6c29d3a..1542bea 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,23 +1,46 @@ mod schema; pub mod users; -use diesel::sqlite::SqliteConnection; use diesel::connection::SimpleConnection; -use diesel::QueryResult; use diesel::r2d2::{ConnectionManager, Pool}; +use diesel::sqlite::{Sqlite, SqliteConnection}; +use std::error::Error; -fn initialize_db(conn: &mut SqliteConnection) -> QueryResult<()> { - // Enable WAL mode and enforce foreign keys - conn.batch_execute("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;") +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(()) } -pub fn initialize_pool(url: &str) -> Pool> { +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).expect("oops"); + let pool = Pool::builder() + .test_on_check_out(true) + .build(manager) + .expect("oops"); - let mut conn = pool.get().unwrap(); - initialize_db(&mut conn).unwrap(); + let mut conn = pool.get()?; + initialize_db(&mut conn)?; - pool + Ok(pool) } diff --git a/src/db/users.rs b/src/db/users.rs index 357ff3b..09360ee 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,12 +1,12 @@ use super::schema::users::{self, dsl::*}; -use diesel::prelude::*; -use diesel::sqlite::SqliteConnection; -use diesel::dsl::{AsSelect, Select}; -use diesel::sqlite::Sqlite; use diesel::dsl::Eq; -use diesel::helper_types::Filter; -use diesel::sql_types::BigInt; +use diesel::dsl::{AsSelect, Select}; use diesel::expression::AsExpression; +use diesel::helper_types::Filter; +use diesel::prelude::*; +use diesel::sql_types::BigInt; +use diesel::sqlite::Sqlite; +use diesel::sqlite::SqliteConnection; #[derive(Queryable, Selectable)] #[diesel(table_name = users)] @@ -44,13 +44,19 @@ impl User { // Self::all().filter(guild_id.eq(guild_id_)) // } - pub fn by_guild_id(guild_id_: i64) -> ByGuild - { + 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 { - Self::all().filter(guild_id.eq(guild_id_)).filter(discord_id.eq(discord_id_)).first(conn) + + pub fn get( + conn: &mut SqliteConnection, + guild_id_: i64, + discord_id_: i64, + ) -> Result { + Self::all() + .filter(guild_id.eq(guild_id_)) + .filter(discord_id.eq(discord_id_)) + .first(conn) } pub fn get_by_id(conn: &mut SqliteConnection, id_: i32) -> Result { diff --git a/src/main.rs b/src/main.rs index c0a9ae7..c1457cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,10 @@ mod commands; mod db; use affluences_api::AffluencesClient; -use poise::serenity_prelude as serenity; -use std::{env::var, time::Duration}; use diesel::r2d2::{ConnectionManager, Pool}; use diesel::sqlite::SqliteConnection; +use poise::serenity_prelude as serenity; +use std::{env::var, time::Duration}; // Types used by all command functions type Error = Box; @@ -14,7 +14,7 @@ type Context<'a> = poise::Context<'a, Data, Error>; // Custom user data passed to all command functions pub struct Data { client: AffluencesClient, - pool: Pool> + pool: Pool>, } async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { @@ -84,6 +84,8 @@ async fn main() { ..Default::default() }; + let pool = db::initialize_pool("affy.db").unwrap(); + poise::Framework::builder() .token( var("DISCORD_TOKEN") @@ -95,7 +97,7 @@ async fn main() { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { client: AffluencesClient::new(), - pool: db::initialize_pool("affy.db") + pool, }) }) }) From 53aeb2339f8a2035543fb0f8be214d865c3fc7bb Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 16 May 2023 16:52:59 +0200 Subject: [PATCH 18/25] feat: bit more robust register --- src/commands/users.rs | 25 +++++++++++++++++-------- src/db/mod.rs | 5 +---- src/db/users.rs | 21 +++++++++++++-------- src/main.rs | 2 +- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/commands/users.rs b/src/commands/users.rs index da968f6..f552787 100644 --- a/src/commands/users.rs +++ b/src/commands/users.rs @@ -10,17 +10,26 @@ pub async fn register( email: String, ) -> Result<(), Error> { if let Some(guild_id) = ctx.guild_id() { - let new_user = NewUser { - discord_id: ctx.author().id.0 as i64, - guild_id: guild_id.into(), - first_name, - last_name, - email, - }; + let discord_id = ctx.author().id.0 as i64; { let mut conn = ctx.data().pool.get()?; - new_user.insert(&mut conn); + + 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?; diff --git a/src/db/mod.rs b/src/db/mod.rs index 1542bea..578506e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -34,10 +34,7 @@ fn initialize_db(conn: &mut SqliteConnection) -> Result<(), DbError> { pub fn initialize_pool(url: &str) -> Result>, DbError> { let manager = ConnectionManager::new(url); - let pool = Pool::builder() - .test_on_check_out(true) - .build(manager) - .expect("oops"); + let pool = Pool::builder().test_on_check_out(true).build(manager)?; let mut conn = pool.get()?; initialize_db(&mut conn)?; diff --git a/src/db/users.rs b/src/db/users.rs index 09360ee..e87f0b9 100644 --- a/src/db/users.rs +++ b/src/db/users.rs @@ -1,14 +1,12 @@ use super::schema::users::{self, dsl::*}; use diesel::dsl::Eq; use diesel::dsl::{AsSelect, Select}; -use diesel::expression::AsExpression; use diesel::helper_types::Filter; use diesel::prelude::*; -use diesel::sql_types::BigInt; use diesel::sqlite::Sqlite; use diesel::sqlite::SqliteConnection; -#[derive(Queryable, Selectable)] +#[derive(Queryable, Selectable, AsChangeset)] #[diesel(table_name = users)] pub struct User { pub id: i32, @@ -52,23 +50,30 @@ impl User { conn: &mut SqliteConnection, guild_id_: i64, discord_id_: i64, - ) -> Result { + ) -> 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 { - Self::all().filter(id.eq(id_)).first(conn) + 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) -> User { + pub fn insert(&self, conn: &mut SqliteConnection) -> Result { diesel::insert_into(users::table) .values(self) .get_result(conn) - .expect("fuck") } } diff --git a/src/main.rs b/src/main.rs index c1457cf..6328584 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,7 +84,7 @@ async fn main() { ..Default::default() }; - let pool = db::initialize_pool("affy.db").unwrap(); + let pool = db::initialize_pool("affy.db").expect("Failed to initialize database."); poise::Framework::builder() .token( From ef7508a456f8aceac7df221d4dd94e19a7044b32 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 07:59:15 +0200 Subject: [PATCH 19/25] feat: move mc stuff to subcommand --- src/commands/minecraft.rs | 12 ++++++++++-- src/commands/mod.rs | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/commands/minecraft.rs b/src/commands/minecraft.rs index fbbedd9..b6250d4 100644 --- a/src/commands/minecraft.rs +++ b/src/commands/minecraft.rs @@ -3,9 +3,17 @@ use async_minecraft_ping::ServerDescription; const DEFAULT_SERVER: &str = "rustybever.be"; -/// Ping a minecraft server +/// 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_mc( +pub async fn ping( ctx: Context<'_>, #[description = "Address of the server"] address: Option, #[description = "Port the server runs on"] port: Option, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 70a5dea..d04b01e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,7 +6,7 @@ use crate::{Context, Data, Error}; type EmbedField = (String, String, bool); pub fn commands() -> Vec> { - vec![help(), affluence::available(), minecraft::ping_mc()] + vec![affluence::available(), minecraft::mc(), help()] } /// Show this help menu @@ -21,7 +21,7 @@ pub async fn 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", + extra_text_at_bottom: "Brought to you by Doofenshmirtz Evil Incorporated.", ..Default::default() }, ) From 83ff00d582d1e6e0f53578474f80b2f656eb97c2 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 10:52:33 +0200 Subject: [PATCH 20/25] feat: add reservation booking --- affluences-api/src/models/available.rs | 9 ++ src/commands/affluence.rs | 70 ----------- src/commands/bib.rs | 158 +++++++++++++++++++++++++ src/commands/mod.rs | 4 +- 4 files changed, 169 insertions(+), 72 deletions(-) delete mode 100644 src/commands/affluence.rs create mode 100644 src/commands/bib.rs diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index 29df5c5..9042c50 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -73,4 +73,13 @@ impl Resource { .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/src/commands/affluence.rs b/src/commands/affluence.rs deleted file mode 100644 index 47d5f9b..0000000 --- a/src/commands/affluence.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::commands::EmbedField; -use crate::{Context, Error}; - -use affluences_api::Resource; -use chrono::NaiveDate; -use uuid::{uuid, Uuid}; - -const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); -const TIME_FORMAT: &str = "%H:%M"; - -fn resource_to_embed_field(resource: Resource) -> EmbedField { - let available_hours = resource.condensed_available_hours(); - - if available_hours.is_empty() { - ( - resource.resource_name.clone(), - "Nothing available.".to_string(), - false, - ) - } else { - ( - resource.resource_name.clone(), - 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: NaiveDate) -> Result<(), Error> { - let client = &ctx.data().client; - let resources = client.available(STERRE_BIB_ID, date, 1).await?; - - ctx.send(|f| { - f.embed(|e| { - e.description(format!("Available booking dates for {}.", date)) - .fields( - resources - .into_iter() - .map(resource_to_embed_field) - .collect::>(), - ) - }) - }) - .await?; - - 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/bib.rs b/src/commands/bib.rs new file mode 100644 index 0000000..244c623 --- /dev/null +++ b/src/commands/bib.rs @@ -0,0 +1,158 @@ +use crate::commands::EmbedField; +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: NaiveDate) -> Result<(), Error> { + let client = &ctx.data().client; + let mut resources = client.available(STERRE_BIB_ID, date, 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 {}.", 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: NaiveDate, + 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, 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, + 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!("{} {} - {}", 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/mod.rs b/src/commands/mod.rs index 40068c5..d32fb16 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ -mod affluence; +mod bib; mod minecraft; mod users; @@ -9,7 +9,7 @@ type EmbedField = (String, String, bool); pub fn commands() -> Vec> { vec![ help(), - affluence::available(), + bib::bib(), minecraft::mc(), users::register(), users::registered(), From 10140d879c25e87498f765ca841e79882fddc911 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 12:17:41 +0200 Subject: [PATCH 21/25] feat: support weekday names & tomorrow --- src/commands/bib.rs | 35 +++++++++++++++++++------------- src/commands/mod.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/commands/bib.rs b/src/commands/bib.rs index 244c623..32fbbe2 100644 --- a/src/commands/bib.rs +++ b/src/commands/bib.rs @@ -1,4 +1,4 @@ -use crate::commands::EmbedField; +use crate::commands::{EmbedField, HumanNaiveDate}; use crate::db::users::User; use crate::{Context, Error}; @@ -46,22 +46,27 @@ fn resource_to_embed_field(resource: Resource) -> EmbedField { /// List available timeslots for day #[poise::command(prefix_command, slash_command)] -pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { +pub async fn available(ctx: Context<'_>, date: HumanNaiveDate) -> Result<(), Error> { let client = &ctx.data().client; - let mut resources = client.available(STERRE_BIB_ID, date, 1).await?; + 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 {}.", date)) - .fields( - resources - .into_iter() - .map(resource_to_embed_field) - .collect::>(), - ) + e.description(format!( + "Available booking dates for {}.", + Into::::into(date) + )) + .fields( + resources + .into_iter() + .map(resource_to_embed_field) + .collect::>(), + ) }) }) .await?; @@ -72,7 +77,7 @@ pub async fn available(ctx: Context<'_>, date: NaiveDate) -> Result<(), Error> { #[poise::command(prefix_command, slash_command)] pub async fn book( ctx: Context<'_>, - date: NaiveDate, + date: HumanNaiveDate, start_time: NaiveTime, end_time: NaiveTime, #[description = "Minimum seats the room should have."] capacity: Option, @@ -102,7 +107,9 @@ pub async fn book( let user = user.unwrap(); let client = &ctx.data().client; - let resources = client.available(STERRE_BIB_ID, date, 1).await?; + 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) @@ -112,7 +119,7 @@ pub async fn book( let reservation = Reservation { auth_type: None, email: user.email.clone(), - date, + date: date.clone().into(), start_time, end_time, note: "coworking space".to_string(), @@ -129,7 +136,7 @@ pub async fn book( ctx.send(|f| { f.embed(|e| { e.description("A new reservation has been made.") - .field("when", format!("{} {} - {}", date, start_time.format(TIME_FORMAT), end_time.format(TIME_FORMAT)), false) + .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))) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d32fb16..f9c81ac 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,10 +2,59 @@ 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.to_string()) + { + 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(|d| HumanNaiveDate(d)) + } + } +} + +impl Into for HumanNaiveDate { + fn into(self) -> chrono::NaiveDate { + self.0 + } +} + pub fn commands() -> Vec> { vec![ help(), From 180768b8588dd806cdc370ea14b5f5d39a64bb25 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 21:14:48 +0200 Subject: [PATCH 22/25] chore: apply clippy's suggestions --- affluences-api/src/models/available.rs | 2 +- src/commands/mod.rs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index 9042c50..e10052c 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -57,7 +57,7 @@ impl Resource { duration = duration + Duration::minutes(hour.granularity.into()); } else { out.push((start_hour, duration)); - start_hour = &hour; + start_hour = hour; duration = Duration::minutes(hour.granularity.into()); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f9c81ac..32551d4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -18,10 +18,7 @@ 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.to_string()) - { + 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 @@ -44,14 +41,14 @@ impl str::FromStr for HumanNaiveDate { now + chrono::Duration::days(days_to_add.into()), )) } else { - chrono::NaiveDate::from_str(s).map(|d| HumanNaiveDate(d)) + chrono::NaiveDate::from_str(s).map(HumanNaiveDate) } } } -impl Into for HumanNaiveDate { - fn into(self) -> chrono::NaiveDate { - self.0 +impl From for chrono::NaiveDate { + fn from(val: HumanNaiveDate) -> chrono::NaiveDate { + val.0 } } From af828dc48e4c74d3ddce2a96088553a707314d30 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 23:03:28 +0200 Subject: [PATCH 23/25] fix: bundle sqlite3 --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/build.rs => build.rs | 0 src/models.rs | 0 4 files changed, 4 insertions(+) rename src/build.rs => build.rs (100%) delete mode 100644 src/models.rs diff --git a/Cargo.lock b/Cargo.lock index b8388a2..1237a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "chrono", "diesel", "diesel_migrations", + "libsqlite3-sys", "poise", "tokio", "uuid", @@ -835,6 +836,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ + "cc", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index cde15dc..518d125 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,5 @@ 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/src/build.rs b/build.rs similarity index 100% rename from src/build.rs rename to build.rs diff --git a/src/models.rs b/src/models.rs deleted file mode 100644 index e69de29..0000000 From 42d1fe82125f2da7869f9f3c58cb0fe1a6d21d28 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 23:18:54 +0200 Subject: [PATCH 24/25] feat(ci): add deploy --- .woodpecker/deploy.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .woodpecker/deploy.yml 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" From 4380074a75f865f12fbc429660c041dc2add238d Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Wed, 17 May 2023 23:21:15 +0200 Subject: [PATCH 25/25] chore: please linter --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 508b53c..3a8149e 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,3 @@ fn main() { - println!("cargo:rerun-if-changed=migrations"); + println!("cargo:rerun-if-changed=migrations"); }