Skip to content

Commit

Permalink
feat: Add tasks from Epitaf's API (#3)
Browse files Browse the repository at this point in the history
* feat: Add tasks from Epitaf's API

* refresh everything every 6 hours and better env handling
  • Loading branch information
kr4xkan authored Jan 4, 2022
1 parent b5d4a98 commit dfbc971
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 14 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ time group is the 2nd one, severity the 3rd one, category 5th one, message 7th o

### Environment variables

* `EPILYON_DONT_FETCH_CRI` - Skip EPITA CRI Intranet fetching when launching to save a lot of time (can't be put on during first launch)
* `EPILYON_DONT_FETCH_EPITAF` - Disables calls to Epitaf's API
* `EPILYON_DONT_SUBSCRIBE` - Prevent Microsoft Graph subscription API from being used (which can't be in localhost)

### Building and running
Expand All @@ -20,8 +20,8 @@ time group is the 2nd one, severity the 3rd one, category 5th one, message 7th o
$ cargo run
```

On the first launch, epilyon.toml will have to be filled before launching a second time.
On the first launch, epilyon.toml will have to be filled before launching a second time.

Normal output should look like this :
Normal output should look like this:

![Normal ouput](https://cdn.discordapp.com/attachments/447725868140331019/679123908854808586/2020-02-18-011526_1270x446_scrot.png)
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub struct EpiConfig {
pub cri_photos_url: String,
pub cri_accessor_username: String,
pub cri_accessor_password: String,

pub epitaf_url: String,
pub epitaf_token: String,

pub ms_tenant_url: String,
pub ms_client_id: String,
Expand Down Expand Up @@ -69,6 +72,9 @@ fn default() -> EpiConfig {
cri_photos_url: "https://cri.photos.url".to_string(),
cri_accessor_username: "firstname.lastname".to_string(),
cri_accessor_password: "password".to_string(),

epitaf_url: "https://api.epitaf.fr/v1".to_string(),
epitaf_token: "your_epitaf_token".to_string(),

ms_tenant_url: "https://login.microsoftonline.com/your_tenant_url".to_string(),
ms_client_id: "your_client_id".to_string(),
Expand Down
40 changes: 35 additions & 5 deletions src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ use actix::prelude::{Actor, Context, AsyncContext};
use failure::Fail;
use lazy_static::lazy_static;

use crate::user::epitaf::{EpitafError, Task};
use crate::user::{User, microsoft, UserError};
use crate::db::{DatabaseConnection, DatabaseError};
use crate::user::microsoft::MSError;
use crate::user::admins::{Delegate, get_admin, get_delegates};
use crate::sync::EpiLock;
use crate::utils::is_env_enable;

pub mod mcq;
pub mod mimos;
pub mod tasks;
mod pdf;
pub mod push_notif;
pub mod subscriptions;
Expand All @@ -44,9 +47,11 @@ use pdf::PDFError;
use mcq::{NextMCQ, MCQResult};
use mimos::Mimos;

use self::tasks::refresh_tasks_db;

pub type DataResult<T> = Result<T, DataError>;

const REFRESH_RATE: u64 = 2 * 24 * 60 * 60; // In seconds (= 2 days)
const REFRESH_RATE: u64 = 6 * 60 * 60; // In seconds (= 6 hours)

lazy_static! {
static ref REFRESH_LOCKS: Mutex<HashMap<String, Arc<Mutex<bool>>>> = Mutex::new(HashMap::new());
Expand All @@ -59,10 +64,13 @@ pub struct UserData {

next_mcq: Option<NextMCQ>,
mcq_history: Vec<MCQResult>,
mimos: Vec<Mimos>
mimos: Vec<Mimos>,
tasks: Vec<Task>
}

pub async fn refresh_all(db: &DatabaseConnection) {
refresh_tasks(&db).await;

let logged_users: Result<Vec<User>, DatabaseError> = db.single_query(
r"
FOR u IN users
Expand Down Expand Up @@ -104,6 +112,23 @@ pub async fn refresh_all(db: &DatabaseConnection) {
}
}

async fn refresh_tasks(db: &DatabaseConnection) {
if is_env_enable("EPILYON_DONT_FETCH_EPITAF") {
return;
}
info!("Refreshing tasks");
let task_refresh = refresh_tasks_db(&db).await;
match task_refresh {
Ok(()) => {
info!("Tasks refreshed");
},
Err(e) => {
error!("Task refreshing error : {}", e.to_detailed_string());
error!("Skipping current refresh");
}
}
}

pub async fn refresh_user(db: &DatabaseConnection, user: &mut User) -> DataResult<()> {
let user_lock = get_user_lock(user);
let guard = user_lock.epilock();
Expand All @@ -125,7 +150,7 @@ pub async fn refresh_user(db: &DatabaseConnection, user: &mut User) -> DataResul

db.update("users", &user.id, user_clone).await?;

if std::env::var("EPILYON_DONT_SUBSCRIBE").is_err() {
if !is_env_enable("EPILYON_DONT_SUBSCRIBE") {
subscriptions::renew_for(db, user, &session.ms_user).await?;
}

Expand Down Expand Up @@ -176,7 +201,8 @@ pub async fn get_data(db: &DatabaseConnection, user: &User) -> DataResult<UserDa

next_mcq: mcq::get_next_mcq(db, user).await?,
mcq_history: mcq::get_mcq_history(db, user).await?,
mimos: mimos::get_mimos(db, user).await?
mimos: mimos::get_mimos(db, user).await?,
tasks: tasks::get_tasks(db, user).await?
})
}

Expand All @@ -189,7 +215,6 @@ impl Actor for RefreshActor {

fn started(&mut self, ctx: &mut Self::Context) {
info!("Started refresh process (every {} seconds)", REFRESH_RATE);

ctx.run_interval(StdDuration::from_secs(REFRESH_RATE), move |a, ctx| {
// We must do this for the reference to be borrowed in the async context
async fn do_refresh(db: DatabaseConnection) {
Expand All @@ -216,6 +241,11 @@ pub enum DataError {
error: MSError
},

#[fail(display = "Epitaf request failed : {}", error)]
EpitafError {
error: EpitafError
},

#[fail(display = "MCQ PDF parsing error : {}", error)]
PDFError {
error: PDFError
Expand Down
71 changes: 71 additions & 0 deletions src/data/tasks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use serde::{Deserialize, Serialize};

use crate::db::DatabaseConnection;
use crate::data::{DataResult, DataError};
use crate::user::User;
use crate::user::epitaf::{Task, fetch_tasks};

#[derive(Clone, Serialize, Deserialize)]
struct PromoTask {
#[serde(rename = "_key")]
promo: String,
tasks: Vec<Task>
}

pub async fn get_tasks(db: &DatabaseConnection, user: &User) -> DataResult<Vec<Task>> {
let all = get_promos_tasks(db, user).await?;
Ok(all.tasks)
}

pub async fn refresh_tasks_db(db: &DatabaseConnection) -> DataResult<()> {
let tasks = fetch_tasks().await;
match tasks {
Ok(t) => {
let mut sorted_tasks: Vec<PromoTask> = Vec::new();
for task in t {
if task.visibility.ne("promotion".into()) {
continue;
}
let check_promo = sorted_tasks
.iter()
.position(|t| t.promo == task.promotion.to_string());
if let Some(p) = check_promo {
sorted_tasks[p].tasks.push(task);
} else {
sorted_tasks.push(
PromoTask {
promo: task.promotion.to_string(),
tasks: Vec::new()
}
);
if let Some (pt) = sorted_tasks.last_mut() {
pt.tasks.push(task);
}
}
}
for st in sorted_tasks {
db.add_or_replace("tasks", st).await?;
}
return Ok(())
},
Err(error) => {
return Err(DataError::EpitafError { error });
}
}
}

async fn get_promos_tasks(db: &DatabaseConnection, user: &User) -> DataResult<PromoTask> {
match db.get::<PromoTask>("tasks", &user.cri_user.promo).await? {
Some(m) => Ok(m),
None => {
let promo_task = PromoTask {
promo: user.cri_user.promo.clone(),
tasks: Vec::new()
};

db.add("tasks", &promo_task).await?;

Ok(promo_task)
}
}
}
4 changes: 2 additions & 2 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pub async fn open(
conn.create_database(database).await?;
}

for col in vec!["users", "next_mcqs", "mcq_histories", "mimos", "options", "admins", "subscriptions"] {
for col in vec!["users", "next_mcqs", "mcq_histories", "mimos", "tasks", "options", "admins", "subscriptions"] {
if !conn.does_collection_exist(col).await? {
info!(" - Creating table '{}'", col);
conn.add_collection(col).await?;
Expand Down Expand Up @@ -195,7 +195,7 @@ impl DatabaseConnection {
{
self.request(
HttpMethod::POST,
&format!("document/{}?override=true", collection),
&format!("document/{}?overwrite=true", collection),
Some(serde_json::to_value(obj)?)
).await?;

Expand Down
8 changes: 4 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const VERSION: &str = "0.1.0";
#[macro_use]
mod macros;

mod utils;

mod data;
mod http;
mod user;
Expand All @@ -33,13 +35,11 @@ mod sync;

use config::CONFIG;
use data::RefreshActor;
use utils::is_env_enable;

#[actix_rt::main]
async fn main() {
let is_debug = match std::env::var("EPILYON_DEBUG") {
Ok(var) => var.to_lowercase() == "true",
Err(_) => false
};
let is_debug = is_env_enable("EPILYON_DEBUG");

if let Err(e) = setup_logger(is_debug) {
eprintln!("Couldn't initialize logger : {}", e);
Expand Down
95 changes: 95 additions & 0 deletions src/user/epitaf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use failure::Fail;
use reqwest::{Client as HttpClient, Error as HttpError};

use crate::config::CONFIG;

#[derive(Clone, Serialize, Deserialize)]
pub struct Task {
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub short_id: String,
pub visibility: String,
pub promotion: u16,
pub semester: Option<String>,
pub class: Option<String>,
pub region: Option<String>,
pub title: String,
pub subject: String,
pub content: String,
pub due_date: DateTime<Utc>,
pub created_by_login: String,
pub created_by: String,
pub updated_by_login: Option<String>,
pub updated_by: Option<String>,
}

#[derive(Deserialize)]
struct LoginResult {
token: String
}

type EpitafResult<T> = Result<T, EpitafError>;

pub async fn fetch_tasks() -> EpitafResult<Vec<Task>> {
let jwt = get_jwt().await?;

let http = HttpClient::new();
let url = format!("{}/tasks", CONFIG.epitaf_url);
let res = http.get(&url)
.header("Accept", "application/json")
.bearer_auth(jwt)
.send().await?
.text().await?;

let value: Vec<Task> = serde_json::from_str(&res)
.map_err(move |e| EpitafError::RemoteError {
method: "get".into(),
url,
request: String::new(),
response: res,
error: e
})?;

Ok(value)
}

async fn get_jwt() -> EpitafResult<String> {
let http = HttpClient::new();
let url = format!("{}/users/callback", CONFIG.epitaf_url);
let res = http.post(&url)
.bearer_auth(&CONFIG.epitaf_token)
.send().await?
.text().await?;

let value: LoginResult = serde_json::from_str(&res)
.map_err(move |e| EpitafError::RemoteError {
method: "post".into(),
url,
request: String::new(),
response: res,
error: e
})?;

Ok(value.token)
}

#[derive(Fail, Debug)]
pub enum EpitafError {
#[fail(display = "HTTP error during request")]
HttpError {
error: HttpError
},

#[fail(display = "Microsoft request was rejected")]
RemoteError {
method: String,
url: String,
request: String,
response: String,
error: serde_json::Error
}
}

from_error!(reqwest::Error, EpitafError, EpitafError::HttpError);
1 change: 1 addition & 0 deletions src/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::db::{DatabaseConnection, DatabaseError};
pub mod cri;
pub mod microsoft;
pub mod admins;
pub mod epitaf;

use cri::CRIError;
use microsoft::MSUser;
Expand Down
8 changes: 8 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub fn is_env_enable(env_name: &str) -> bool {
match std::env::var(env_name) {
Ok(s) => {
vec!["1", "true", "yes", "oui"].contains(&s.to_lowercase().as_str())
},
Err(_) => false
}
}

0 comments on commit dfbc971

Please sign in to comment.