diff --git a/affluences-api/src/lib.rs b/affluences-api/src/lib.rs index 395dacc..17c7ace 100644 --- a/affluences-api/src/lib.rs +++ b/affluences-api/src/lib.rs @@ -1,9 +1,10 @@ mod models; -pub use models::*; use chrono::NaiveDate; +pub use models::*; -const USER_AGENT: &str = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0"; +const USER_AGENT: &str = + "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0"; pub struct AffluencesClient { client: reqwest::Client, @@ -13,22 +14,61 @@ impl AffluencesClient { pub fn new() -> Self { Self { client: reqwest::Client::builder() - .user_agent(USER_AGENT).build().unwrap(), + .user_agent(USER_AGENT) + .build() + .unwrap(), } } - pub async fn available(&self, site_id: uuid::Uuid, date: NaiveDate, resource_type: u32) -> reqwest::Result> { - let url = format!("https://reservation.affluences.com/api/resources/{}/available", site_id); - self.client.get(url).query(&[("date", date.format("%Y-%m-%d").to_string()), ("type", resource_type.to_string())]).send().await?.json::>().await + pub async fn available( + &self, + site_id: uuid::Uuid, + date: NaiveDate, + resource_type: u32, + ) -> reqwest::Result> { + let url = format!( + "https://reservation.affluences.com/api/resources/{}/available", + site_id + ); + self.client + .get(url) + .query(&[ + ("date", date.format("%Y-%m-%d").to_string()), + ("type", resource_type.to_string()), + ]) + .send() + .await? + .json::>() + .await } pub async fn site_data(&self, slug: &str) -> reqwest::Result { let url = format!("https://api.affluences.com/app/v3/sites/{}", slug); - Ok(self.client.get(url).send().await?.json::>().await?.data) + Ok(self + .client + .get(url) + .send() + .await? + .json::>() + .await? + .data) } - pub async fn make_reservation(&self, resource_id: u32, reservation: &Reservation) -> reqwest::Result { - let url = format!("https://reservation.affluences.com/api/reserve/{}", resource_id); - self.client.post(url).json(reservation).send().await?.json::().await + pub async fn make_reservation( + &self, + resource_id: u32, + reservation: &Reservation, + ) -> reqwest::Result { + let url = format!( + "https://reservation.affluences.com/api/reserve/{}", + resource_id + ); + self.client + .post(url) + .json(reservation) + .send() + .await? + .json::() + .await } } diff --git a/affluences-api/src/models.rs b/affluences-api/src/models.rs deleted file mode 100644 index 0dceb3d..0000000 --- a/affluences-api/src/models.rs +++ /dev/null @@ -1,206 +0,0 @@ -use serde::{Deserialize, Serialize}; -use chrono::{NaiveTime, NaiveDate}; - -mod time_format { - use chrono::NaiveTime; - use serde::{self, Deserialize, Serializer, Deserializer}; - - const FORMAT: &'static str = "%H:%M"; - - pub fn serialize( - time: &NaiveTime, - serializer: S, - ) -> Result - where - S: Serializer, - { - let s = format!("{}", time.format(FORMAT)); - serializer.serialize_str(&s) - } - - pub fn deserialize<'de, D>( - deserializer: D, - ) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - NaiveTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom) - } -} - -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct Hour { - #[serde(with = "time_format")] - pub hour: NaiveTime, - pub state: u32, - // reservations - pub granularity: u32, - pub person_count: u32, - pub places_available: u32, - pub places_bookable: u32, -} - -#[derive(Deserialize, Debug)] -pub struct Resource { - pub resource_id: u32, - pub resource_name: String, - pub resource_type: u32, - pub granularity: u32, - pub time_slot_count: u32, - pub static_time_slot: bool, - // reservations_by_timeslot - pub note_available: bool, - pub note_required: bool, - pub note_description: String, - pub description: String, - pub capacity: u32, - pub site_timezone: String, - pub user_name_required: bool, - pub user_phone_required: bool, - pub user_name_available: bool, - pub user_phone_available: bool, - // time_before_reservations_closed - // min_places_per_reservation - // max_places_per_reservation - pub image_url: Option, - // services - pub slots_state: u32, - pub hours: Vec, -} - -#[derive(Deserialize, Debug)] -pub struct Data { - pub data: T -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataCategory { - pub id: u32, - pub name: String, - pub name_plural: String, -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataLocationCoordinates { - pub latitude: f64, - pub longitude: f64, -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataLocationAddress { - pub route: String, - pub city: String, - pub zip_code: String, - pub region: String, - pub country_code: String -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataLocation { - pub coordinates: SiteDataLocationCoordinates, - pub address: SiteDataLocationAddress -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataForecast { - pub opened: bool, - pub occupancy: u32, - // waiting_time - pub waiting_time_overflow: bool -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataNotice { - pub message: String, - pub url: Option -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataService { - pub id: u32, - pub name: String, -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataInfo { - pub title: String, - pub description: String, - pub url: Option, -} - -#[derive(Deserialize, Debug)] -pub struct SiteDataStatus { - pub state: String, - pub text: String, - pub color: String, -} - -#[derive(Deserialize, Debug)] -pub struct SiteData { - pub id: uuid::Uuid, - pub slug: String, - pub parent: Option, - pub primary_name: String, - pub secondary_name: String, - pub concat_name: String, - pub categories: Vec, - pub time_zone: String, - pub location: SiteDataLocation, - pub phone_number: Option, - pub email: Option, - pub url: Option, - pub notices: Vec, - // messages - pub estimated_distance: f64, - pub current_forecast: SiteDataForecast, - pub today_forecasts: Vec, - // events - pub children: Vec, - // actions - pub services: Vec, - pub infos: Vec, - pub poster_image: String, - pub image: Option>, - // status - pub closed: bool, - pub booking_available: bool, - pub extended_forecasts: bool, - pub booking_url: Option, - pub validated: bool, - #[serde(rename = "validationStatus")] - pub validation_status: String, - #[serde(rename = "publicationStatus")] - pub publication_status: String -} - -#[derive(Serialize, Debug)] -pub struct Reservation { - // This string might not be correct - pub auth_type: Option, - pub email: String, - pub date: NaiveDate, - #[serde(with = "time_format")] - pub start_time: NaiveTime, - #[serde(with = "time_format")] - pub end_time: NaiveTime, - pub note: String, - pub user_firstname: String, - pub user_lastname: String, - pub user_phone: Option, - pub person_count: u32 -} - -#[derive(Deserialize, Debug)] -pub struct ReservationResponse { - pub reservation_id: u32, - // This string might not be correct - pub auth_type: Option, - pub user_validation: bool, - // ticket_payload - pub email: String, - pub success: String, - #[serde(rename = "successMessage")] - pub success_message: String, - // cancellation_token -} diff --git a/affluences-api/src/models/available.rs b/affluences-api/src/models/available.rs new file mode 100644 index 0000000..4b63f5f --- /dev/null +++ b/affluences-api/src/models/available.rs @@ -0,0 +1,43 @@ +use super::hh_mm_time_format; +use chrono::NaiveTime; +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct HourBlock { + #[serde(with = "hh_mm_time_format")] + pub hour: NaiveTime, + pub state: u32, + // reservations + pub granularity: u32, + pub person_count: u32, + pub places_available: u32, + pub places_bookable: u32, +} + +#[derive(Deserialize, Debug)] +pub struct Resource { + pub resource_id: u32, + pub resource_name: String, + pub resource_type: u32, + pub granularity: u32, + pub time_slot_count: u32, + pub static_time_slot: bool, + // reservations_by_timeslot + pub note_available: bool, + pub note_required: bool, + pub note_description: String, + pub description: String, + pub capacity: u32, + pub site_timezone: String, + pub user_name_required: bool, + pub user_phone_required: bool, + pub user_name_available: bool, + pub user_phone_available: bool, + // time_before_reservations_closed + // min_places_per_reservation + // max_places_per_reservation + pub image_url: Option, + // services + pub slots_state: u32, + pub hours: Vec, +} diff --git a/affluences-api/src/models/hh_mm_time_format.rs b/affluences-api/src/models/hh_mm_time_format.rs new file mode 100644 index 0000000..7d535f6 --- /dev/null +++ b/affluences-api/src/models/hh_mm_time_format.rs @@ -0,0 +1,20 @@ +use chrono::NaiveTime; +use serde::{self, Deserialize, Deserializer, Serializer}; + +const FORMAT: &'static str = "%H:%M"; + +pub fn serialize(time: &NaiveTime, serializer: S) -> Result +where + S: Serializer, +{ + let s = format!("{}", time.format(FORMAT)); + serializer.serialize_str(&s) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + NaiveTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom) +} diff --git a/affluences-api/src/models/mod.rs b/affluences-api/src/models/mod.rs new file mode 100644 index 0000000..0ccda39 --- /dev/null +++ b/affluences-api/src/models/mod.rs @@ -0,0 +1,8 @@ +mod available; +mod hh_mm_time_format; +mod reservation; +mod site_data; + +pub use available::*; +pub use reservation::*; +pub use site_data::*; diff --git a/affluences-api/src/models/reservation.rs b/affluences-api/src/models/reservation.rs new file mode 100644 index 0000000..739934b --- /dev/null +++ b/affluences-api/src/models/reservation.rs @@ -0,0 +1,34 @@ +use super::hh_mm_time_format; +use chrono::{NaiveDate, NaiveTime}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +pub struct Reservation { + // This string might not be correct + pub auth_type: Option, + pub email: String, + pub date: NaiveDate, + #[serde(with = "hh_mm_time_format")] + pub start_time: NaiveTime, + #[serde(with = "hh_mm_time_format")] + pub end_time: NaiveTime, + pub note: String, + pub user_firstname: String, + pub user_lastname: String, + pub user_phone: Option, + pub person_count: u32, +} + +#[derive(Deserialize, Debug)] +pub struct ReservationResponse { + pub reservation_id: u32, + // This string might not be correct + pub auth_type: Option, + pub user_validation: bool, + // ticket_payload + pub email: String, + pub success: String, + #[serde(rename = "successMessage")] + pub success_message: String, + // cancellation_token +} diff --git a/affluences-api/src/models/site_data.rs b/affluences-api/src/models/site_data.rs new file mode 100644 index 0000000..1d13a22 --- /dev/null +++ b/affluences-api/src/models/site_data.rs @@ -0,0 +1,106 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct Data { + pub data: T, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataCategory { + pub id: u32, + pub name: String, + pub name_plural: String, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataLocationCoordinates { + pub latitude: f64, + pub longitude: f64, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataLocationAddress { + pub route: String, + pub city: String, + pub zip_code: String, + pub region: String, + pub country_code: String, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataLocation { + pub coordinates: SiteDataLocationCoordinates, + pub address: SiteDataLocationAddress, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataForecast { + pub opened: bool, + pub occupancy: u32, + // waiting_time + pub waiting_time_overflow: bool, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataNotice { + pub message: String, + pub url: Option, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataService { + pub id: u32, + pub name: String, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataInfo { + pub title: String, + pub description: String, + pub url: Option, +} + +#[derive(Deserialize, Debug)] +pub struct SiteDataStatus { + pub state: String, + pub text: String, + pub color: String, +} + +#[derive(Deserialize, Debug)] +pub struct SiteData { + pub id: uuid::Uuid, + pub slug: String, + pub parent: Option, + pub primary_name: String, + pub secondary_name: String, + pub concat_name: String, + pub categories: Vec, + pub time_zone: String, + pub location: SiteDataLocation, + pub phone_number: Option, + pub email: Option, + pub url: Option, + pub notices: Vec, + // messages + pub estimated_distance: f64, + pub current_forecast: SiteDataForecast, + pub today_forecasts: Vec, + // events + pub children: Vec, + // actions + pub services: Vec, + pub infos: Vec, + pub poster_image: String, + pub image: Option>, + // status + pub closed: bool, + pub booking_available: bool, + pub extended_forecasts: bool, + pub booking_url: Option, + pub validated: bool, + #[serde(rename = "validationStatus")] + pub validation_status: String, + #[serde(rename = "publicationStatus")] + pub publication_status: String, +} diff --git a/bot/src/commands.rs b/bot/src/commands.rs index 1dad50b..60d82d5 100644 --- a/bot/src/commands.rs +++ b/bot/src/commands.rs @@ -1,8 +1,7 @@ use crate::{Context, Error}; -use chrono::{NaiveDate, Duration}; +use affluences_api::HourBlock; +use chrono::{Duration, NaiveDate}; use uuid::{uuid, Uuid}; -use affluences_api::Hour; -use poise::serenity_prelude as serenity; const STERRE_BIB_ID: Uuid = uuid!("4737e57a-ee05-4f7b-901a-7bb541eeb297"); const TIME_FORMAT: &'static str = "%H:%M"; @@ -85,24 +84,25 @@ pub async fn getvotes( /// 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: NaiveDate) -> Result<(), Error> { let client = &ctx.data().client; let resources = client.available(STERRE_BIB_ID, date, 1).await?; let mut fields: Vec<(String, String, bool)> = Default::default(); for resource in &resources { if resource.hours.len() == 0 { - fields.push((resource.resource_name.clone(), "Nothing available.".to_string(), false)); + fields.push(( + resource.resource_name.clone(), + "Nothing available.".to_string(), + false, + )); - continue + continue; } let mut lines: Vec = Default::default(); - let mut start_hour_opt: Option<&Hour> = None; + let mut start_hour_opt: Option<&HourBlock> = None; let mut duration = Duration::seconds(0); for hour in &resource.hours { @@ -111,7 +111,13 @@ pub async fn available( duration = duration + Duration::minutes(hour.granularity.into()); } else { let end_hour = start_hour.hour + duration; - lines.push(format!("{} - {} ({:02}:{:02})", start_hour.hour.format(TIME_FORMAT), end_hour.format(TIME_FORMAT), duration.num_hours(), duration.num_minutes() % 60)); + lines.push(format!( + "{} - {} ({:02}:{:02})", + start_hour.hour.format(TIME_FORMAT), + end_hour.format(TIME_FORMAT), + duration.num_hours(), + duration.num_minutes() % 60 + )); start_hour_opt = None; } } else if hour.state == 1 { @@ -123,16 +129,32 @@ pub async fn available( // 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)); + lines.push(format!( + "{} - {} ({:02}:{:02})", + start_hour.hour.format(TIME_FORMAT), + end_hour.format(TIME_FORMAT), + duration.num_hours(), + duration.num_minutes() % 60 + )); } fields.push((resource.resource_name.clone(), lines.join("\n"), false)); } - ctx.send(|f| - f.embed(|e| - e.description(format!("Available booking dates for {}.", date)) - .fields(fields))).await?; + ctx.send(|f| { + f.embed(|e| { + e.description(format!("Available booking dates for {}.", date)) + .fields(fields) + }) + }) + .await?; Ok(()) } + +// Create a reservation +// #[poise::command(prefix_command, slash_command)] +// pub async fn reserve( +// ctx: Context<'_>, +// date: NaiveDate, +// ) -> Result<(), Error> { diff --git a/bot/src/main.rs b/bot/src/main.rs index f7d482a..f6e06bb 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -36,7 +36,12 @@ 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::vote(), + commands::getvotes(), + commands::available(), + ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("~".into()), edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600))),