diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 7adfe62..5779344 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -5,7 +5,7 @@ branches: pipeline: build: - image: 'rust:1.76' + image: 'rust:1.69' commands: - cargo build --verbose - cargo test --verbose diff --git a/.woodpecker/clippy.yml b/.woodpecker/clippy.yml index d34cdf6..92851e5 100644 --- a/.woodpecker/clippy.yml +++ b/.woodpecker/clippy.yml @@ -5,7 +5,7 @@ branches: pipeline: clippy: - image: 'rust:1.76' + image: 'rust:1.69' commands: - rustup component add clippy - cargo clippy -- --no-deps -Dwarnings diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml deleted file mode 100644 index 7be1e36..0000000 --- a/.woodpecker/deploy.yml +++ /dev/null @@ -1,22 +0,0 @@ -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 index 89c1d62..ad2b612 100644 --- a/.woodpecker/lint.yml +++ b/.woodpecker/lint.yml @@ -5,7 +5,7 @@ branches: pipeline: lint: - image: 'rust:1.76' + image: 'rust:1.69' commands: - rustup component add rustfmt - cargo fmt -- --check diff --git a/Cargo.lock b/Cargo.lock index 31b1f33..b8388a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,11 +23,9 @@ name = "affluences-cli" version = "0.1.0" dependencies = [ "affluences-api", - "chrono", "clap", "serde_json", "tokio", - "uuid", ] [[package]] @@ -39,7 +37,6 @@ dependencies = [ "chrono", "diesel", "diesel_migrations", - "libsqlite3-sys", "poise", "tokio", "uuid", @@ -838,7 +835,6 @@ 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 b41350e..cde15dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,3 @@ 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"] } - -# https://stackoverflow.com/a/54842093 -[profile.release] -lto = true # Enable link-time optimization -codegen-units = 1 # Reduce number of codegen units to increase optimizations -panic = 'abort' # Abort on panic -strip = true # Strip symbols from binary* diff --git a/Dockerfile b/Dockerfile index 12e9300..b2cbacc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.76-alpine AS builder +FROM rust:1.69-alpine AS builder ARG DI_VER=1.2.5 @@ -25,9 +25,6 @@ FROM busybox:1.36.0 COPY --from=builder /build/dumb-init /build/affy/target/release/affy /bin/ -RUN mkdir /data && \ - chown -R www-data:www-data /data - WORKDIR /data ENV TZ=Europe/Brussels diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs index ced3836..29df5c5 100644 --- a/affluences-api/src/models/available.rs +++ b/affluences-api/src/models/available.rs @@ -1,19 +1,12 @@ use super::hh_mm_time_format; use chrono::{Duration, NaiveTime}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum ResourceState { - Available, - Full, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Deserialize, Debug, Clone, Copy)] pub struct HourBlock { #[serde(with = "hh_mm_time_format")] pub hour: NaiveTime, - pub state: ResourceState, + pub state: u32, // reservations pub granularity: u32, pub person_count: u32, @@ -21,7 +14,7 @@ pub struct HourBlock { pub places_bookable: u32, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Deserialize, Debug)] pub struct Resource { pub resource_id: u32, pub resource_name: String, @@ -45,7 +38,7 @@ pub struct Resource { // max_places_per_reservation pub image_url: Option, // services - pub slots_state: ResourceState, + pub slots_state: u32, pub hours: Vec, } @@ -64,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()); } } @@ -77,21 +70,7 @@ impl Resource { pub fn condensed_available_hours(&self) -> Vec<(&HourBlock, Duration)> { self.condensed_hours() .into_iter() - .filter(|(hour, _)| hour.state == ResourceState::Available) + .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: ResourceState, - ) -> 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-cli/Cargo.toml b/affluences-cli/Cargo.toml index f9aa8b1..f46a340 100644 --- a/affluences-cli/Cargo.toml +++ b/affluences-cli/Cargo.toml @@ -10,5 +10,3 @@ affluences-api = { path = "../affluences-api" } clap = { version = "4.2.7", features = ["derive"] } serde_json = "1.0.96" tokio = { version = "1.28.1", features = ["full"] } -uuid = "*" -chrono = "*" diff --git a/affluences-cli/src/main.rs b/affluences-cli/src/main.rs index afd9b29..3b31cb5 100644 --- a/affluences-cli/src/main.rs +++ b/affluences-cli/src/main.rs @@ -1,9 +1,5 @@ use affluences_api::AffluencesClient; -use chrono::NaiveDate; use clap::{Parser, Subcommand}; -use uuid::{uuid, Uuid}; - -const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -15,10 +11,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { /// does testing things - SearchSite { - query: String, - }, - Available, + SearchSite { query: String }, } #[tokio::main] @@ -32,14 +25,6 @@ async fn main() { let s = serde_json::to_string_pretty(&res).unwrap(); println!("{}", s); } - Some(Commands::Available) => { - let res = client - .available(STERRE_BIB_ID, chrono::Utc::now().naive_utc().date(), 1) - .await - .unwrap(); - let s = serde_json::to_string_pretty(&res).unwrap(); - println!("{}", s); - } None => {} } } diff --git a/build.rs b/build.rs deleted file mode 100644 index 3a8149e..0000000 --- a/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("cargo:rerun-if-changed=migrations"); -} 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/affluence.rs b/src/commands/affluence.rs new file mode 100644 index 0000000..47d5f9b --- /dev/null +++ b/src/commands/affluence.rs @@ -0,0 +1,70 @@ +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 deleted file mode 100644 index f5f68f7..0000000 --- a/src/commands/bib.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::commands::{EmbedField, HumanNaiveDate}; -use crate::db::users::User; -use crate::{Context, Error}; - -use affluences_api::{Reservation, Resource, ResourceState}; -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 date: NaiveDate = date.into(); - - 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: 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 date: NaiveDate = date.into(); - - 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, ResourceState::Available)); - - 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/minecraft.rs b/src/commands/minecraft.rs index b6250d4..fbbedd9 100644 --- a/src/commands/minecraft.rs +++ b/src/commands/minecraft.rs @@ -3,17 +3,9 @@ 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. +/// Ping a minecraft server #[poise::command(prefix_command, slash_command)] -pub async fn ping( +pub async fn ping_mc( 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 32551d4..d0f47ce 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,62 +1,16 @@ -mod bib; +mod affluence; 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(), + affluence::available(), + minecraft::ping_mc(), users::register(), users::registered(), ] @@ -74,7 +28,7 @@ pub async fn help( ctx, command.as_deref(), poise::builtins::HelpConfiguration { - extra_text_at_bottom: "Brought to you by Doofenshmirtz Evil Incorporated.", + extra_text_at_bottom: "This is an example bot made to showcase features of my custom Discord bot framework", ..Default::default() }, ) diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..e69de29