epitech_api/
lib.rs

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}