1use std::fmt;
2use std::future::Future;
3use std::pin::Pin;
4use std::str::FromStr;
5
6use chrono::prelude::*;
7use enum_iterator::IntoEnumIterator;
8use reqwest::header;
9use serde::{Deserialize, Serialize};
10
11pub mod error;
12pub mod response;
13
14#[cfg(test)]
15mod tests;
16
17use crate::error::Error;
18
19pub static ENDPOINT: &str = "https://intra.epitech.eu";
20
21#[derive(Debug, Clone, Default)]
22pub struct ClientBuilder {
23 autologin: String,
24 retry_count: u32,
25}
26
27#[derive(Debug, Clone)]
28pub struct Client {
29 autologin: String,
30 retry_count: u32,
31 client: reqwest::Client,
32 login: String,
33}
34
35#[derive(
36 Debug,
37 Clone,
38 Copy,
39 PartialEq,
40 PartialOrd,
41 Eq,
42 Ord,
43 Hash,
44 Serialize,
45 Deserialize,
46 IntoEnumIterator,
47)]
48pub enum Location {
49 #[serde(rename = "ES/BAR")]
50 Barcelone,
51 #[serde(rename = "DE/BER")]
52 Berlin,
53 #[serde(rename = "FR/BDX")]
54 Bordeaux,
55 #[serde(rename = "FR/RUN")]
56 LaReunion,
57 #[serde(rename = "FR/LIL")]
58 Lille,
59 #[serde(rename = "FR/LYN")]
60 Lyon,
61 #[serde(rename = "FR/MAR")]
62 Marseille,
63 #[serde(rename = "FR/MPL")]
64 Montpellier,
65 #[serde(rename = "FR/NCY")]
66 Nancy,
67 #[serde(rename = "FR/NAN")]
68 Nantes,
69 #[serde(rename = "FR/NCE")]
70 Nice,
71 #[serde(rename = "FR/PAR")]
72 Paris,
73 #[serde(rename = "FR/REN")]
74 Rennes,
75 #[serde(rename = "FR/STG")]
76 Strasbourg,
77 #[serde(rename = "FR/TLS")]
78 Toulouse,
79 #[serde(rename = "BJ/COT")]
80 Cotonou,
81 #[serde(rename = "AL/TIR")]
82 Tirana,
83 #[serde(rename = "BE/BRU")]
84 Bruxelles,
85}
86
87#[derive(
88 Debug,
89 Clone,
90 Copy,
91 PartialEq,
92 PartialOrd,
93 Eq,
94 Ord,
95 Hash,
96 Serialize,
97 Deserialize,
98 IntoEnumIterator,
99)]
100pub enum Promo {
101 #[serde(rename = "tek1")]
102 Tek1,
103 #[serde(rename = "tek2")]
104 Tek2,
105 #[serde(rename = "tek3")]
106 Tek3,
107 #[serde(rename = "wac1")]
108 Wac1,
109 #[serde(rename = "wac2")]
110 Wac2,
111 #[serde(rename = "msc3")]
112 Msc3,
113 #[serde(rename = "msc4")]
114 Msc4,
115}
116
117#[derive(Debug, Clone, Default)]
118pub struct StudentListFetchBuilder {
119 client: Client,
120 location: Option<Location>,
121 promo: Option<Promo>,
122 year: u32,
123 course: Option<String>,
124 active: bool,
125 offset: u32,
126}
127
128#[derive(Debug, Clone, Default)]
129pub struct StudentDataFetchBuilder {
130 client: Client,
131 login: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135struct UserEntries {
136 pub total: usize,
137 pub items: Vec<response::UserEntry>,
138}
139
140impl ClientBuilder {
141 pub fn new() -> ClientBuilder {
142 ClientBuilder {
143 autologin: String::default(),
144 retry_count: 5,
145 }
146 }
147
148 #[inline]
149 pub fn autologin<T: Into<String>>(mut self, autologin: T) -> ClientBuilder {
150 self.autologin = autologin.into();
151 self
152 }
153
154 #[inline]
155 pub fn retry_count(mut self, retry_count: u32) -> ClientBuilder {
156 self.retry_count = retry_count;
157 self
158 }
159
160 pub async fn authenticate(self) -> Result<Client, Error> {
161 let client = match reqwest::Client::builder()
162 .redirect(reqwest::redirect::Policy::none())
163 .build()
164 {
165 Ok(x) => x,
166 Err(_) => return Err(Error::InternalError),
167 };
168 match client.get(&self.autologin).send().await {
169 Ok(response) => {
170 let headers = response.headers();
171 let cookie = headers
172 .get_all(header::SET_COOKIE)
173 .iter()
174 .filter_map(|it| it.to_str().ok())
175 .find(|cookie| cookie.starts_with("user="))
176 .and_then(|cookie| cookie.split(';').nth(0))
177 .and_then(|cookie| header::HeaderValue::from_str(cookie).ok())
178 .ok_or(Error::CookieNotFound)?;
179
180 let mut headers = header::HeaderMap::new();
181 headers.insert(header::COOKIE, cookie);
182 let autologin = self.autologin;
183 let retry_count = self.retry_count;
184 let client = reqwest::Client::builder()
185 .default_headers(headers)
186 .build()
187 .map_err(|_| Error::InternalError)?;
188 let login = String::default();
189 let mut client = Client {
190 autologin,
191 retry_count,
192 client,
193 login,
194 };
195 let data = client.fetch_student_data().send().await?;
196 client.login = data.login.clone();
197 Ok(client)
198 }
199 Err(err) => {
200 let status = err.status();
201 match status {
202 Some(status) => Err(Error::InvalidStatusCode(status.as_u16())),
203 None => Err(Error::UnreachableRemote),
204 }
205 }
206 }
207 }
208}
209
210impl Client {
211 #[inline]
212 pub fn builder() -> ClientBuilder {
213 ClientBuilder::new()
214 }
215
216 pub async fn make_request<T: ToString>(&self, url: T) -> Result<String, Error> {
217 let mut string = url.to_string();
218 if !string.contains("&format=json") && !string.contains("?format=json") {
219 let b = string.contains('?');
220 string.push(if b { '&' } else { '?' });
221 string.push_str("format=json");
222 }
223 if !string.starts_with(ENDPOINT) {
224 string.insert_str(0, ENDPOINT);
225 }
226 for _ in 0..self.retry_count {
227 let result = self.client.get(&string).send().await;
228 let result = match result {
229 Ok(val) => val.text().await,
230 Err(err) => Err(err),
231 };
232 if let Ok(body) = result {
233 return Ok(body);
234 }
235 }
236 Err(Error::RetryLimit)
237 }
238
239 pub fn fetch_student_list(&self) -> StudentListFetchBuilder {
240 StudentListFetchBuilder::new().client(self.clone())
241 }
242
243 pub fn fetch_student_data(&self) -> StudentDataFetchBuilder {
244 StudentDataFetchBuilder::new().client(self.clone())
245 }
246
247 pub async fn fetch_student_netsoul<'a>(
248 &self,
249 login: &'a str,
250 ) -> Result<Vec<response::UserNetsoulEntry>, Error> {
251 let url = format!("/user/{}/netsoul", login);
252 let response = self.make_request(url).await?;
253 let data = json::from_str(&response)?;
254 Ok(data)
255 }
256
257 pub async fn fetch_own_student_netsoul(
258 &self,
259 ) -> Result<Vec<response::UserNetsoulEntry>, Error> {
260 self.fetch_student_netsoul(self.login.as_ref()).await
261 }
262
263 pub async fn fetch_student_notes<'a>(
264 &self,
265 login: &'a str,
266 ) -> Result<response::UserNotes, Error> {
267 let url = format!("/user/{}/notes", login);
268 let response = self.make_request(url).await?;
269 let data = json::from_str(&response)?;
270 Ok(data)
271 }
272
273 pub async fn fetch_own_student_notes(&self) -> Result<response::UserNotes, Error> {
274 self.fetch_student_notes(self.login.as_ref()).await
275 }
276
277 pub async fn fetch_student_binomes<'a>(
278 &self,
279 login: &'a str,
280 ) -> Result<response::UserBinome, Error> {
281 let url = format!("/user/{}/binome", login);
282 let response = self.make_request(url).await?;
283 let data = json::from_str(&response)?;
284 Ok(data)
285 }
286
287 pub async fn fetch_own_student_binomes(&self) -> Result<response::UserBinome, Error> {
288 self.fetch_student_binomes(self.login.as_ref()).await
289 }
290
291 pub async fn search_student(
292 &self,
293 login: &str,
294 ) -> Result<Vec<response::UserSearchResultEntry>, Error> {
295 let url = format!("/complete/user?format=json&contains&search={}", login);
296 let response = self.make_request(url).await?;
297 let data = json::from_str(&response)?;
298 Ok(data)
299 }
300
301 pub async fn fetch_available_courses(
302 &self,
303 location: Location,
304 year: u32,
305 active: bool,
306 ) -> Result<Vec<response::AvailableCourseEntry>, Error> {
307 let url = format!(
308 "/user/filter/course?format=json&location={}&year={}&active={}",
309 location, year, active
310 );
311 let response = self.make_request(url).await?;
312 let data = json::from_str(&response)?;
313 Ok(data)
314 }
315
316 pub async fn fetch_available_promos(
317 &self,
318 location: Location,
319 year: u32,
320 course: &str,
321 active: bool,
322 ) -> Result<Vec<response::AvailablePromoEntry>, Error> {
323 let url = format!(
324 "/user/filter/promo?format=json&location={}&year={}&course={}&active={}",
325 location, year, course, active
326 );
327 let response = self.make_request(url).await?;
328 let data = json::from_str(&response)?;
329 Ok(data)
330 }
331}
332
333impl Default for Client {
334 #[inline]
335 fn default() -> Client {
336 Client {
337 autologin: String::default(),
338 retry_count: 5,
339 client: reqwest::Client::new(),
340 login: String::default(),
341 }
342 }
343}
344
345impl StudentListFetchBuilder {
346 #[inline]
347 pub fn new() -> StudentListFetchBuilder {
348 StudentListFetchBuilder {
349 client: Client::default(),
350 location: None,
351 promo: None,
352 active: true,
353 offset: 0,
354 year: Local::now().date().year() as u32,
355 course: None,
356 }
357 }
358
359 fn send_impl(self) -> Pin<Box<dyn Future<Output = Result<Vec<response::UserEntry>, Error>>>> {
360 Box::pin(async move {
361 let mut url = format!(
362 "/user/filter/user?offset={}&year={}&active={}",
363 self.offset, self.year, self.active,
364 );
365 if let Some(ref location) = self.location {
366 url = format!("{}&location={}", url, location);
367 }
368 if let Some(ref promo) = self.promo {
369 url = format!("{}&promo={}", url, promo);
370 }
371 if let Some(ref course) = self.course {
372 url = format!("{}&course={}", url, course);
373 }
374 let response = self.client.make_request(url).await?;
375 let mut data = json::from_str::<UserEntries>(&response)?;
376 let state: usize = (self.offset as usize) + data.items.len();
377 if state == data.total {
378 Ok(data.items)
379 } else if state >= data.total {
380 Err(Error::InternalError)
381 } else {
382 let mut additional = self.offset(state as u32).send_impl().await?;
383 data.items.append(&mut additional);
384 Ok(data.items)
385 }
386 })
387 }
388
389 pub async fn send(self) -> Result<Vec<response::UserEntry>, Error> {
390 self.send_impl().await
391 }
392
393 #[inline]
394 pub fn client(mut self, client: Client) -> StudentListFetchBuilder {
395 self.client = client;
396 self
397 }
398
399 #[inline]
400 pub fn location(mut self, location: Location) -> StudentListFetchBuilder {
401 self.location = Some(location);
402 self
403 }
404
405 #[inline]
406 pub fn active(mut self, active: bool) -> StudentListFetchBuilder {
407 self.active = active;
408 self
409 }
410
411 #[inline]
412 pub fn offset(mut self, offset: u32) -> StudentListFetchBuilder {
413 self.offset = offset;
414 self
415 }
416
417 #[inline]
418 pub fn year(mut self, year: u32) -> StudentListFetchBuilder {
419 self.year = year;
420 self
421 }
422
423 #[inline]
424 pub fn promo(mut self, promo: Promo) -> StudentListFetchBuilder {
425 self.promo = Some(promo);
426 self
427 }
428
429 #[inline]
430 pub fn course<T: Into<String>>(mut self, course: T) -> StudentListFetchBuilder {
431 self.course = Some(course.into());
432 self
433 }
434}
435
436impl StudentDataFetchBuilder {
437 #[inline]
438 pub fn new() -> StudentDataFetchBuilder {
439 StudentDataFetchBuilder {
440 client: Client::default(),
441 login: None,
442 }
443 }
444
445 pub async fn send(self) -> Result<response::UserData, Error> {
446 let url = self
447 .login
448 .map(|login| format!("/user/{}", login))
449 .unwrap_or_else(|| String::from("/user"));
450 let response = self.client.make_request(url).await?;
451 let data = json::from_str(&response)?;
452 Ok(data)
453 }
454
455 #[inline]
456 pub fn client(mut self, client: Client) -> StudentDataFetchBuilder {
457 self.client = client;
458 self
459 }
460
461 #[inline]
462 pub fn login<T: Into<String>>(mut self, login: T) -> StudentDataFetchBuilder {
463 self.login = Some(login.into());
464 self
465 }
466}
467
468impl FromStr for Location {
469 type Err = ();
470 fn from_str(string: &str) -> Result<Self, Self::Err> {
471 match string {
472 "ES/BAR" => Ok(Location::Barcelone),
473 "DE/BER" => Ok(Location::Berlin),
474 "FR/BDX" => Ok(Location::Bordeaux),
475 "FR/RUN" => Ok(Location::LaReunion),
476 "FR/LIL" => Ok(Location::Lille),
477 "FR/LYN" => Ok(Location::Lyon),
478 "FR/MAR" => Ok(Location::Marseille),
479 "FR/MPL" => Ok(Location::Montpellier),
480 "FR/NCY" => Ok(Location::Nancy),
481 "FR/NAN" => Ok(Location::Nantes),
482 "FR/NCE" => Ok(Location::Nice),
483 "FR/PAR" => Ok(Location::Paris),
484 "FR/REN" => Ok(Location::Rennes),
485 "FR/STG" => Ok(Location::Strasbourg),
486 "FR/TLS" => Ok(Location::Toulouse),
487 "BJ/COT" => Ok(Location::Cotonou),
488 "AL/TIR" => Ok(Location::Tirana),
489 "BE/BRU" => Ok(Location::Bruxelles),
490 _ => Err(()),
491 }
492 }
493}
494
495impl fmt::Display for Location {
496 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
497 let repr = match self {
498 Location::Barcelone => "ES/BAR",
499 Location::Berlin => "DE/BER",
500 Location::Bordeaux => "FR/BDX",
501 Location::LaReunion => "FR/RUN",
502 Location::Lille => "FR/LIL",
503 Location::Lyon => "FR/LYN",
504 Location::Marseille => "FR/MAR",
505 Location::Montpellier => "FR/MPL",
506 Location::Nancy => "FR/NCY",
507 Location::Nantes => "FR/NAN",
508 Location::Nice => "FR/NCE",
509 Location::Paris => "FR/PAR",
510 Location::Rennes => "FR/REN",
511 Location::Strasbourg => "FR/STG",
512 Location::Toulouse => "FR/TLS",
513 Location::Bruxelles => "BE/BRU",
514 Location::Cotonou => "BJ/COT",
515 Location::Tirana => "AL/TIR",
516 };
517 write!(f, "{}", repr)
518 }
519}
520
521impl FromStr for Promo {
522 type Err = ();
523 fn from_str(string: &str) -> Result<Self, Self::Err> {
524 match string {
525 "tek1" => Ok(Promo::Tek1),
526 "tek2" => Ok(Promo::Tek2),
527 "tek3" => Ok(Promo::Tek3),
528 "wac1" => Ok(Promo::Wac1),
529 "wac2" => Ok(Promo::Wac2),
530 "msc3" => Ok(Promo::Msc3),
531 "msc4" => Ok(Promo::Msc4),
532 _ => Err(()),
533 }
534 }
535}
536
537impl fmt::Display for Promo {
538 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
539 let repr = match self {
540 Promo::Tek1 => "tek1",
541 Promo::Tek2 => "tek2",
542 Promo::Tek3 => "tek3",
543 Promo::Wac1 => "wac1",
544 Promo::Wac2 => "wac2",
545 Promo::Msc3 => "msc3",
546 Promo::Msc4 => "msc4",
547 };
548 write!(f, "{}", repr)
549 }
550}