Compare commits
	
		
			2 commits
		
	
	
		
			36e10cf8d2
			...
			d6dc15b4a3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d6dc15b4a3 | ||
|   | 1b73fe2cbb | 
					 7 changed files with 408 additions and 6 deletions
				
			
		
							
								
								
									
										16
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | # Khollisé Worker | ||||||
|  | 
 | ||||||
|  | ## Job Types | ||||||
|  | 
 | ||||||
|  | | job_id | description                                                      | | ||||||
|  | |--------|------------------------------------------------------------------| | ||||||
|  | | 0      | Fetch ONE colle (with class_name, colle_id and colle_secret)     | | ||||||
|  | | 1      | Fetch class colles (with class_name and optionally go to date)   | | ||||||
|  | | 2      | Fetch upcoming colles (with class_name)                          | | ||||||
|  | | 3      | Fetch sumbittable meals                                          | | ||||||
|  | | 4      | Submit meals for user (must include bj_username and bj_password) | | ||||||
|  | | 5      | Test authentication (must include bj_username and bj_password)   | | ||||||
|  | 
 | ||||||
|  | Actually, all jobs require `class_name` to be specified. | ||||||
|  | 
 | ||||||
|  | <!-- TODO: Add documentation for used Redis keys --> | ||||||
							
								
								
									
										74
									
								
								src/api.rs
									
										
									
									
									
								
							
							
						
						
									
										74
									
								
								src/api.rs
									
										
									
									
									
								
							|  | @ -70,6 +70,74 @@ pub async fn post_upcoming_colles( | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | pub async fn post_submittable_meals( | ||||||
|  |     meals: &[Meal], | ||||||
|  |     config: &Settings, | ||||||
|  | ) -> Result<(), Box<dyn std::error::Error>> { | ||||||
|  |     let api_url = get_config(config, "api"); | ||||||
|  |     let api_token = get_config(config, "token"); | ||||||
|  | 
 | ||||||
|  |     let url = format!("{api_url}/meals"); | ||||||
|  |     let meals_json = json!({ | ||||||
|  |       "meals": &meals | ||||||
|  |     }); | ||||||
|  |     println!("Posting submittable meals: {:?}", meals_json); | ||||||
|  | 
 | ||||||
|  |     let response = Client::new() | ||||||
|  |         .post(&url) | ||||||
|  |         .json(&meals_json) | ||||||
|  |         .header("Content-Type", "application/json") | ||||||
|  |         .header("Accept", "application/json") | ||||||
|  |         // Bearer token for authentication
 | ||||||
|  |         .header("Authorization", format!("Bearer {api_token}")) | ||||||
|  |         .send() | ||||||
|  |         .await?; | ||||||
|  |     // Check if the response is successful
 | ||||||
|  |     if !response.status().is_success() { | ||||||
|  |         eprintln!( | ||||||
|  |             "Failed to post submittable meals: HTTP {}", | ||||||
|  |             response.status(), | ||||||
|  |         ); | ||||||
|  |         eprint!("Response: {:?}", response.text().await?); | ||||||
|  |         return Err("Failed to post submittable meals".into()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn post_selected_meals( | ||||||
|  |     meals: &[Meal], | ||||||
|  |     user_id: &i64, | ||||||
|  |     config: &Settings, | ||||||
|  | ) -> Result<(), Box<dyn std::error::Error>> { | ||||||
|  |     let api_url = get_config(config, "api"); | ||||||
|  |     let api_token = get_config(config, "token"); | ||||||
|  | 
 | ||||||
|  |     let url = format!("{api_url}/internals/meals-registrations"); | ||||||
|  |     let meals_json = json!({ | ||||||
|  |         "meals": &meals, | ||||||
|  |         "userId": user_id, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let response = Client::new() | ||||||
|  |         .post(&url) | ||||||
|  |         .json(&meals_json) | ||||||
|  |         .header("Content-Type", "application/json") | ||||||
|  |         .header("Accept", "application/json") | ||||||
|  |         // Bearer token for authentication
 | ||||||
|  |         .header("Authorization", format!("Bearer {api_token}")) | ||||||
|  |         .send() | ||||||
|  |         .await?; | ||||||
|  |     // Check if the response is successful
 | ||||||
|  |     if !response.status().is_success() { | ||||||
|  |         eprintln!("Failed to post selected meals: HTTP {}", response.status(),); | ||||||
|  |         eprint!("Response: {:?}", response.text().await?); | ||||||
|  |         return Err("Failed to post selected meals".into()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Debug, serde::Serialize)] | #[derive(Debug, serde::Serialize)] | ||||||
| pub struct Colle { | pub struct Colle { | ||||||
|     pub date: NaiveDateTime, |     pub date: NaiveDateTime, | ||||||
|  | @ -90,3 +158,9 @@ pub struct ColleAttachment { | ||||||
|     pub url: String, |     pub url: String, | ||||||
|     pub name: String, |     pub name: String, | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, serde::Serialize)] | ||||||
|  | pub struct Meal { | ||||||
|  |     pub date: String, // "YYYY-MM-DD"
 | ||||||
|  |     pub meal_type: String, // "lunch" or "dinner"
 | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										145
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										145
									
								
								src/main.rs
									
										
									
									
									
								
							|  | @ -1,9 +1,12 @@ | ||||||
| use std::{str::FromStr, sync::Arc}; | use std::{str::FromStr, sync::Arc}; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|     api::{post_colle, post_upcoming_colles}, |     api::{Meal, post_colle, post_selected_meals, post_submittable_meals, post_upcoming_colles}, | ||||||
|     configuration::{get_config, get_cron, list_classes, load_config}, |     configuration::{get_config, get_cron, list_classes, load_config}, | ||||||
|     parser::{authenticate, fetch_class_colles, fetch_colle, fetch_upcoming_colles}, |     parser::{ | ||||||
|  |         authenticate, fetch_class_colles, fetch_colle, fetch_upcoming_colles, get_available_meals, | ||||||
|  |         login, request_session, | ||||||
|  |     }, | ||||||
| }; | }; | ||||||
| use chrono::{DateTime, Utc}; | use chrono::{DateTime, Utc}; | ||||||
| use cron::Schedule; | use cron::Schedule; | ||||||
|  | @ -179,6 +182,117 @@ async fn process_job( | ||||||
|                 eprintln!("Failed to fetch colle {}: {}", colle_id, e); |                 eprintln!("Failed to fetch colle {}: {}", colle_id, e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |     } else if job["type"] == 4 || job["type"] == 5 { | ||||||
|  |         let user_id = job["user_id"].as_number().unwrap().as_i64().unwrap_or(-1); | ||||||
|  |         if user_id == -1 { | ||||||
|  |             eprintln!("Job does not contain a valid user ID."); | ||||||
|  |             return Err(redis::RedisError::from(( | ||||||
|  |                 redis::ErrorKind::InvalidClientConfig, | ||||||
|  |                 "Job missing valid user ID", | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Authenticate (job data contains password and username)
 | ||||||
|  |         let username = job["bj_username"].as_str().unwrap_or_default(); | ||||||
|  |         let password = job["bj_password"].as_str().unwrap_or_default(); | ||||||
|  |         if username.is_empty() || password.is_empty() { | ||||||
|  |             eprintln!("Job does not contain username or password."); | ||||||
|  |             return Err(redis::RedisError::from(( | ||||||
|  |                 redis::ErrorKind::InvalidClientConfig, | ||||||
|  |                 "Job missing username or password", | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let session = request_session().await; | ||||||
|  |         if let Err(err) = session { | ||||||
|  |             eprintln!("Failed to request session: {}", err); | ||||||
|  |             return Err(redis::RedisError::from(( | ||||||
|  |                 redis::ErrorKind::AuthenticationFailed, | ||||||
|  |                 "Session request failed", | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  |         let session_id = session.unwrap(); | ||||||
|  | 
 | ||||||
|  |         let login_result = login(&username, &password, &session_id).await; | ||||||
|  |         if let Err(err) = login_result { | ||||||
|  |             if job["type"] == 5 { | ||||||
|  |                 // Set auth failed in Redis
 | ||||||
|  |                 let auth_key = format!("auth_success_{}", user_id); | ||||||
|  |                 con.set_ex(&auth_key, "ERROR", 300)?; | ||||||
|  |             } | ||||||
|  |             eprintln!("Failed to login: {}", err); | ||||||
|  |             return Err(redis::RedisError::from(( | ||||||
|  |                 redis::ErrorKind::AuthenticationFailed, | ||||||
|  |                 "Login failed", | ||||||
|  |             ))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if job["type"] == 4 { | ||||||
|  |             // Fetch already selected meals
 | ||||||
|  |             let selected_meals = get_available_meals(&session_id, true).await; | ||||||
|  |             if let Err(err) = selected_meals { | ||||||
|  |                 eprintln!("Failed to fetch available meals: {}", err); | ||||||
|  |                 return Err(redis::RedisError::from(( | ||||||
|  |                     redis::ErrorKind::ResponseError, | ||||||
|  |                     "Failed to fetch available meals", | ||||||
|  |                 ))); | ||||||
|  |             } | ||||||
|  |             let selected_meals = selected_meals.unwrap(); | ||||||
|  | 
 | ||||||
|  |             // Add job["meal"] (ONLY ONE MEAL) to selected meals
 | ||||||
|  |             let mut meals_parsed = selected_meals; | ||||||
|  |             let meal_to_add = Meal { | ||||||
|  |                 date: job["meal"]["date"].as_str().unwrap_or_default().to_string(), | ||||||
|  |                 meal_type: job["meal"]["meal_type"] | ||||||
|  |                     .as_str() | ||||||
|  |                     .unwrap_or("dinner") | ||||||
|  |                     .to_string(), | ||||||
|  |             }; | ||||||
|  |             if !meals_parsed | ||||||
|  |                 .iter() | ||||||
|  |                 .any(|m| m.date == meal_to_add.date && m.meal_type == meal_to_add.meal_type) | ||||||
|  |             { | ||||||
|  |                 meals_parsed.push(meal_to_add); | ||||||
|  | 
 | ||||||
|  |                 // Submit meals
 | ||||||
|  |                 let res = parser::submit_meals(&meals_parsed, &session_id).await; | ||||||
|  |                 if res.is_err() { | ||||||
|  |                     eprintln!("Failed to submit meals: {}", res.unwrap_err()); | ||||||
|  |                     return Err(redis::RedisError::from(( | ||||||
|  |                         redis::ErrorKind::ResponseError, | ||||||
|  |                         "Failed to submit meals", | ||||||
|  |                     ))); | ||||||
|  |                 } | ||||||
|  |                 println!("Meals submitted successfully."); | ||||||
|  |             } else { | ||||||
|  |                 println!("Meal already selected, skipping addition."); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let meals = get_available_meals(&session_id, true).await; | ||||||
|  |             if meals.is_err() { | ||||||
|  |                 eprintln!("Failed to fetch available meals: {}", meals.unwrap_err()); | ||||||
|  |                 return Err(redis::RedisError::from(( | ||||||
|  |                     redis::ErrorKind::ResponseError, | ||||||
|  |                     "Failed to fetch available meals", | ||||||
|  |                 ))); | ||||||
|  |             } | ||||||
|  |             let meals = meals.unwrap(); | ||||||
|  | 
 | ||||||
|  |             // Post selected meals to API
 | ||||||
|  |             let res = post_selected_meals(&meals, &user_id, config).await; | ||||||
|  |             if res.is_err() { | ||||||
|  |                 eprintln!("Failed to post selected meals: {}", res.unwrap_err()); | ||||||
|  |                 return Err(redis::RedisError::from(( | ||||||
|  |                     redis::ErrorKind::ResponseError, | ||||||
|  |                     "Failed to post selected meals", | ||||||
|  |                 ))); | ||||||
|  |             } | ||||||
|  |             println!("Posted {} selected meals successfully.", meals.len()); | ||||||
|  |         } else if job["type"] == 5 { | ||||||
|  |             // Set auth successful in Redis
 | ||||||
|  |             let auth_key = format!("auth_success_{}", user_id); | ||||||
|  |             con.set_ex(&auth_key, "SUCCESS", 300)?; | ||||||
|  |         } | ||||||
|     } else { |     } else { | ||||||
|         // Authenticate
 |         // Authenticate
 | ||||||
|         let session = authenticate(class_name, &mut *con, config).await; |         let session = authenticate(class_name, &mut *con, config).await; | ||||||
|  | @ -256,6 +370,33 @@ async fn process_job( | ||||||
|                 ))); |                 ))); | ||||||
|             } |             } | ||||||
|             println!("Posted upcoming colles successfully."); |             println!("Posted upcoming colles successfully."); | ||||||
|  |         } else if job["type"] == 3 { | ||||||
|  |             println!("Fetching available meals..."); | ||||||
|  | 
 | ||||||
|  |             /* | ||||||
|  |              * Fetch available meals | ||||||
|  |              */ | ||||||
|  |             match get_available_meals(&session, false).await { | ||||||
|  |                 Ok(meals) => { | ||||||
|  |                     if meals.is_empty() { | ||||||
|  |                         println!("No available meals found."); | ||||||
|  |                         return Ok(()); | ||||||
|  |                     } | ||||||
|  |                     // Post available meals to API
 | ||||||
|  |                     let res = post_submittable_meals(&meals, config).await; | ||||||
|  |                     if res.is_err() { | ||||||
|  |                         eprintln!("Failed to post submittable meals: {}", res.unwrap_err()); | ||||||
|  |                         return Err(redis::RedisError::from(( | ||||||
|  |                             redis::ErrorKind::ResponseError, | ||||||
|  |                             "Failed to post submittable meals", | ||||||
|  |                         ))); | ||||||
|  |                     } | ||||||
|  |                     println!("Posted {} submittable meals successfully.", meals.len()); | ||||||
|  |                 } | ||||||
|  |                 Err(e) => { | ||||||
|  |                     eprintln!("Failed to fetch available meals: {}", e); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         /* |         /* | ||||||
|          * Handle unknown job types |          * Handle unknown job types | ||||||
|          * This is a catch-all for any job types that are not recognized. |          * This is a catch-all for any job types that are not recognized. | ||||||
|  |  | ||||||
|  | @ -59,7 +59,7 @@ pub async fn login( | ||||||
|         .collect(); |         .collect(); | ||||||
| 
 | 
 | ||||||
|     if session_id.is_empty() { |     if session_id.is_empty() { | ||||||
|         return Err("Failed to get session ID".into()); |         return Err("Invalid credentials".into()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     Ok(session_id.join("; ")) |     Ok(session_id.join("; ")) | ||||||
|  |  | ||||||
|  | @ -1,9 +1,15 @@ | ||||||
| mod auth; | mod auth; | ||||||
| mod utils; | mod utils; | ||||||
| mod colles; | mod colles; | ||||||
|  | mod repas; | ||||||
| 
 | 
 | ||||||
| pub use colles::fetch_class_colles; | pub use colles::fetch_class_colles; | ||||||
| pub use colles::fetch_colle; | pub use colles::fetch_colle; | ||||||
| pub use colles::fetch_upcoming_colles; | pub use colles::fetch_upcoming_colles; | ||||||
| 
 | 
 | ||||||
| pub use auth::authenticate; | pub use auth::authenticate; | ||||||
|  | pub use auth::login; | ||||||
|  | pub use auth::request_session; | ||||||
|  | 
 | ||||||
|  | pub use repas::get_available_meals; | ||||||
|  | pub use repas::submit_meals; | ||||||
|  |  | ||||||
							
								
								
									
										161
									
								
								src/parser/repas.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/parser/repas.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,161 @@ | ||||||
|  | use std::collections::HashMap; | ||||||
|  | 
 | ||||||
|  | use chrono::{Datelike, NaiveDate}; | ||||||
|  | use reqwest::Client; | ||||||
|  | use scraper::{Html, Selector}; | ||||||
|  | 
 | ||||||
|  | use crate::{api::Meal, parser::utils::parse_french_date}; | ||||||
|  | 
 | ||||||
|  | pub async fn get_available_meals( | ||||||
|  |     session: &str, | ||||||
|  |     filter_selected: bool, | ||||||
|  | ) -> Result<Vec<Meal>, Box<dyn std::error::Error>> { | ||||||
|  |     let url = "https://bjcolle.fr/no_waste.php"; | ||||||
|  |     let response = Client::new() | ||||||
|  |         .get(url) | ||||||
|  |         .header("Cookie", session) | ||||||
|  |         .send() | ||||||
|  |         .await? | ||||||
|  |         .text() | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |     let document = Html::parse_document(&response); | ||||||
|  | 
 | ||||||
|  |     // Select each table row
 | ||||||
|  |     let row_selector = Selector::parse("#Choix > table > tbody > tr").unwrap(); | ||||||
|  |     let label_selector = Selector::parse("label").unwrap(); | ||||||
|  |     let input_selector = Selector::parse("input[type=\"checkbox\"]").unwrap(); | ||||||
|  | 
 | ||||||
|  |     let mut meals = Vec::new(); | ||||||
|  | 
 | ||||||
|  |     for row in document.select(&row_selector) { | ||||||
|  |         // Extract the label text (meal name)
 | ||||||
|  |         if let Some(label) = row.select(&label_selector).next() { | ||||||
|  |             let meal_name = label | ||||||
|  |                 .text() | ||||||
|  |                 .collect::<Vec<_>>() | ||||||
|  |                 .join(" ") | ||||||
|  |                 .trim() | ||||||
|  |                 .to_string(); | ||||||
|  | 
 | ||||||
|  |             // Extract the input checkbox
 | ||||||
|  |             if let Some(input) = row.select(&input_selector).next() { | ||||||
|  |                 let is_checked = input.value().attr("checked").is_some(); | ||||||
|  | 
 | ||||||
|  |                 // Apply filtering: if filter_selected = true, skip unchecked
 | ||||||
|  |                 if filter_selected && !is_checked { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if !meal_name.is_empty() { | ||||||
|  |                     if let Some(meal) = parse_meal(&meal_name) { | ||||||
|  |                         meals.push(meal); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(meals) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn parse_meal(name: &str) -> Option<Meal> { | ||||||
|  |     // Normalize input
 | ||||||
|  |     let trimmed = name.trim(); | ||||||
|  | 
 | ||||||
|  |     // Detect meal type
 | ||||||
|  |     let meal_type = if trimmed.starts_with("Déjeuner") { | ||||||
|  |         "lunch" | ||||||
|  |     } else if trimmed.starts_with("Dîner") { | ||||||
|  |         "dinner" | ||||||
|  |     } else { | ||||||
|  |         return None; // Unknown meal type
 | ||||||
|  |     } | ||||||
|  |     .to_string(); | ||||||
|  | 
 | ||||||
|  |     // Extract date string (after "du ") and format it YYYY-MM-DD
 | ||||||
|  |     let date_part = trimmed.splitn(2, "du ").nth(1)?; | ||||||
|  |     let date = parse_french_date(date_part).unwrap(); | ||||||
|  |     let date = date.format("%Y-%m-%d").to_string(); | ||||||
|  | 
 | ||||||
|  |     Some(Meal { date, meal_type }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Map chrono weekday to French first letter
 | ||||||
|  | fn french_day_letter(date: NaiveDate) -> char { | ||||||
|  |     match date.weekday() { | ||||||
|  |         chrono::Weekday::Mon => 'l', // Lundi
 | ||||||
|  |         chrono::Weekday::Tue => 'm', // Mardi
 | ||||||
|  |         chrono::Weekday::Wed => 'm', // Mercredi
 | ||||||
|  |         chrono::Weekday::Thu => 'j', // Jeudi
 | ||||||
|  |         chrono::Weekday::Fri => 'v', // Vendredi
 | ||||||
|  |         chrono::Weekday::Sat => 's', // Samedi
 | ||||||
|  |         chrono::Weekday::Sun => 'd', // Dimanche
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Map chrono month to French month name
 | ||||||
|  | fn french_month(month: u32) -> &'static str { | ||||||
|  |     match month { | ||||||
|  |         1 => "janvier", | ||||||
|  |         2 => "février", | ||||||
|  |         3 => "mars", | ||||||
|  |         4 => "avril", | ||||||
|  |         5 => "mai", | ||||||
|  |         6 => "juin", | ||||||
|  |         7 => "juillet", | ||||||
|  |         8 => "août", | ||||||
|  |         9 => "septembre", | ||||||
|  |         10 => "octobre", | ||||||
|  |         11 => "novembre", | ||||||
|  |         12 => "décembre", | ||||||
|  |         _ => "", | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn submit_meals(meals: &[Meal], session: &str) -> Result<(), Box<dyn std::error::Error>> { | ||||||
|  |     let url = "https://bjcolle.fr/no_waste.php"; | ||||||
|  |     let client = Client::new(); | ||||||
|  | 
 | ||||||
|  |     let mut form: HashMap<String, String> = HashMap::new(); | ||||||
|  | 
 | ||||||
|  |     // Mandatory field
 | ||||||
|  |     form.insert( | ||||||
|  |         "VALIDER_CHOIX".to_string(), | ||||||
|  |         "Valider+(même+si+rien+n'est+coché)".to_string(), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     for meal in meals { | ||||||
|  |         let meal_date = NaiveDate::parse_from_str(&meal.date, "%Y-%m-%d")?; | ||||||
|  |         let day_letter = french_day_letter(meal_date); | ||||||
|  | 
 | ||||||
|  |         let key = match meal.meal_type.as_str() { | ||||||
|  |             "lunch" => format!("{}m", day_letter),  // midi
 | ||||||
|  |             "dinner" => format!("{}s", day_letter), // soir
 | ||||||
|  |             other => { | ||||||
|  |                 eprintln!("Unknown meal_type: {}", other); | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Format like "30+août"
 | ||||||
|  |         let value = format!("{}+{}", meal_date.day(), french_month(meal_date.month())); | ||||||
|  | 
 | ||||||
|  |         form.insert(key, value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let res = client | ||||||
|  |         .post(url) | ||||||
|  |         .header("Cookie", session) | ||||||
|  |         .form(&form) | ||||||
|  |         .send() | ||||||
|  |         .await?; | ||||||
|  | 
 | ||||||
|  |     if !res.status().is_success() { | ||||||
|  |         eprintln!("Failed to submit meals: HTTP {}", res.status()); | ||||||
|  |         eprint!("Response: {:?}", res.text().await?); | ||||||
|  |         return Err("Failed to submit meals".into()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| use chrono::{NaiveDate, NaiveDateTime, NaiveTime}; | use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime}; | ||||||
| use sha2::{Digest, Sha256}; | use sha2::{Digest, Sha256}; | ||||||
| use std::collections::HashMap; | use std::collections::HashMap; | ||||||
| use std::error::Error; | use std::error::Error; | ||||||
|  | @ -24,7 +24,7 @@ pub fn parse_french_date(french_date: &str) -> Result<NaiveDate, Box<dyn Error>> | ||||||
| 
 | 
 | ||||||
|     // Split by spaces
 |     // Split by spaces
 | ||||||
|     let parts: Vec<&str> = french_date.split_whitespace().collect(); |     let parts: Vec<&str> = french_date.split_whitespace().collect(); | ||||||
|     if parts.len() < 4 { |     if parts.len() < 3 { | ||||||
|         return Err("Date string too short".into()); |         return Err("Date string too short".into()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -42,7 +42,11 @@ pub fn parse_french_date(french_date: &str) -> Result<NaiveDate, Box<dyn Error>> | ||||||
|         .ok_or_else(|| format!("Unknown month: {}", parts[2]))?; |         .ok_or_else(|| format!("Unknown month: {}", parts[2]))?; | ||||||
| 
 | 
 | ||||||
|     // Year
 |     // Year
 | ||||||
|     let year: i32 = parts[3].parse()?; |     let year = if let Some(s) = parts.get(3) { | ||||||
|  |         s.parse()? | ||||||
|  |     } else { | ||||||
|  |         chrono::Local::now().year() | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     let date = NaiveDate::from_ymd_opt(year, *month, day).ok_or("Invalid date components")?; |     let date = NaiveDate::from_ymd_opt(year, *month, day).ok_or("Invalid date components")?; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue