Compare commits
No commits in common. "python" and "main" have entirely different histories.
9 changed files with 2114 additions and 117 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1,9 +1 @@
|
||||||
/.vscode
|
/target
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
||||||
*.html
|
|
||||||
|
|
||||||
bin/
|
|
||||||
lib/
|
|
||||||
pyvenv.cfg
|
|
||||||
|
|
1874
Cargo.lock
generated
Normal file
1874
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "uwm"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Mylloon"]
|
||||||
|
edition = "2021"
|
||||||
|
description = "Retrieves grades from the Paris 8 website"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://git.kennel.ml/Anri/UWM"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
directories = "4.0.1"
|
||||||
|
toml = "0.5.10"
|
||||||
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.11", features = ["cookies"] }
|
||||||
|
tokio = { version = ">=1.23.1", features = ["full"] }
|
||||||
|
scraper = "0.14"
|
||||||
|
clap = { version = "4.0.32", features = ["derive"] }
|
||||||
|
rpassword = "7.2"
|
29
README.md
29
README.md
|
@ -1,15 +1,30 @@
|
||||||
# Wrapper de notes pour [uPortal](https://github.com/uPortal-Project/uPortal-start)
|
# UWM
|
||||||
|
|
||||||
## Utilisation
|
Récupère les notes depuis le site de Paris 8
|
||||||
|
|
||||||
|
[![dependency status](https://deps.rs/repo/gitea/git.kennel.ml/Anri/uportalWrapperMarks/status.svg)](https://deps.rs/repo/gitea/git.kennel.ml/Anri/uportalWrapperMarks)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### En ligne de commande
|
||||||
|
|
||||||
```
|
```
|
||||||
python3 main.py "<lien-vers-l'instance-CAS-pour-la-connexion-à-uPortal>" "<pseudo>" "<mot-de-passe>"
|
$ uwm username
|
||||||
|
|
||||||
# Exemple URL : https://cas.XXX.xxx/cas/login?service=https://e-p8.XXX.xxx/uPortal/Login
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Vous pouvez aussi utilisez un fichier `.env` (recommandé) avec : `URL`, `LOGIN` et `PASSWORD` comme nom de variables.
|
### Avec un fichier de configuration `config.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
username = "username" # Exemple pour Alice Dupont : adupont
|
||||||
|
password = "password" # Facultatif
|
||||||
|
```
|
||||||
|
|
||||||
|
_Attention : le mot de passe est stocké en clair_
|
||||||
|
|
||||||
|
- Linux : `~/.config/uwm/config.toml`
|
||||||
|
- Windows : `%APPDATA%\anri\uwm\config.toml`
|
||||||
|
- MacOS : `~/Library/Application Support/com.anri.uwm/config.toml` <!-- i guess? -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Testé avec l'[instance de Paris 8](https://e-p8.univ-paris8.fr).
|
Anciennement [uPortalWrapperMarks](https://git.kennel.ml/Anri/uportalWrapperMarks/src/branch/python).
|
||||||
|
|
99
main.py
99
main.py
|
@ -1,99 +0,0 @@
|
||||||
from dotenv import load_dotenv
|
|
||||||
from os import environ
|
|
||||||
from sys import argv
|
|
||||||
|
|
||||||
from requests_html import HTMLSession
|
|
||||||
|
|
||||||
|
|
||||||
class Universite:
|
|
||||||
def __init__(self, url: str, pseudo: str, motDePasse: str):
|
|
||||||
self.url = url
|
|
||||||
self.loginData = {
|
|
||||||
"username": pseudo,
|
|
||||||
"password": motDePasse,
|
|
||||||
"_eventId": "submit",
|
|
||||||
"submit": "SE CONNECTER"
|
|
||||||
}
|
|
||||||
|
|
||||||
def ecrirePageHTML(self, nom: str, texte: str):
|
|
||||||
"""Affiche la page HTML pour le debug."""
|
|
||||||
with open(f"{nom}.html", 'w') as f:
|
|
||||||
f.write(texte)
|
|
||||||
|
|
||||||
def recuperationNotes(self) -> dict:
|
|
||||||
"""Récupère les notes sous forme d'un dictionnaire."""
|
|
||||||
with HTMLSession() as session:
|
|
||||||
reponse = session.get(self.url)
|
|
||||||
|
|
||||||
# login
|
|
||||||
self.loginData["lt"] = [element.attrs["value"] for element in reponse.html.find(
|
|
||||||
"input") if element.attrs["name"] == "lt"][0]
|
|
||||||
self.loginData["execution"] = [element.attrs["value"] for element in reponse.html.find(
|
|
||||||
"input") if element.attrs["name"] == "execution"][0]
|
|
||||||
reponse = session.post(self.url, data=self.loginData)
|
|
||||||
|
|
||||||
# page des résultats intermédiaire
|
|
||||||
try:
|
|
||||||
url = [element.attrs["href"] for element in reponse.html.find(
|
|
||||||
'a') if "id" in element.attrs if element.attrs["id"] == "service-407"][0]
|
|
||||||
except IndexError: # Arrive quand "An Error Has Occurred"
|
|
||||||
raise TimeoutError(
|
|
||||||
"Le site a prit trop de temps pour répondre, veuillez réessayez plus tard.")
|
|
||||||
reponse = session.get(url, allow_redirects=False)
|
|
||||||
url = reponse.headers["Location"]
|
|
||||||
reponse = session.get(url)
|
|
||||||
|
|
||||||
# choix des années
|
|
||||||
url = f"{url}?{[element.attrs['action'] for element in reponse.html.find('form') if 'enctype' in element.attrs if element.attrs['enctype'] == 'application/x-www-form-urlencoded'][0].split('?')[1].replace('welcome', 'notes')}"
|
|
||||||
reponse = session.get(url)
|
|
||||||
anneesTemp = [element for element in reponse.html.find(
|
|
||||||
'a') if "href" in element.attrs if element.attrs["href"] == '#'][6:]
|
|
||||||
|
|
||||||
# on retire un item sur deux car : ['L2MINF/210', 'L2 Informatique', 'L1MINF/210', 'L1 Informatique'] -> il y a des doublons
|
|
||||||
annees = []
|
|
||||||
for i in range(0, len(anneesTemp)):
|
|
||||||
if i % 2:
|
|
||||||
annees.append(anneesTemp[i])
|
|
||||||
|
|
||||||
# récupération notes
|
|
||||||
resultat = {}
|
|
||||||
for annee in annees:
|
|
||||||
resultat[annee.text] = None
|
|
||||||
|
|
||||||
reponse.html.render(script=annee.attrs["onclick"][7:])
|
|
||||||
reponse = session.post(url.replace(
|
|
||||||
"render", "action").replace("detail", ""))
|
|
||||||
|
|
||||||
reponse = session.get(url.replace(
|
|
||||||
"action", "render").replace("notes", "detailnotes"))
|
|
||||||
self.ecrirePageHTML(annee.text, reponse.text)
|
|
||||||
|
|
||||||
return resultat
|
|
||||||
|
|
||||||
def affichageNotes(self, notes: dict) -> str:
|
|
||||||
"""Renvoie un jolie tableau avec les notes"""
|
|
||||||
return str(notes)
|
|
||||||
|
|
||||||
def notes(self):
|
|
||||||
"""Affiche les notes dans stdout."""
|
|
||||||
print(self.affichageNotes(self.recuperationNotes()))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
nom = argv.pop(0)
|
|
||||||
if len(argv) == 3:
|
|
||||||
Universite(*argv).notes()
|
|
||||||
else:
|
|
||||||
load_dotenv()
|
|
||||||
try:
|
|
||||||
Universite(environ["URL"], environ["LOGIN"],
|
|
||||||
environ["PASSWORD"]).notes()
|
|
||||||
except KeyError:
|
|
||||||
raise Exception(f"""
|
|
||||||
\nMerci de renseigner l'URL, le pseudo et le mot de passe (avec des \"). \
|
|
||||||
\n-> python3 {nom} "URL" "pseudo" "mot-de-passe" \
|
|
||||||
\n--- \
|
|
||||||
\nOu fichier .env contenant ses informations avec les noms (conseillé) : \
|
|
||||||
\n-> URL \
|
|
||||||
\n-> LOGIN \
|
|
||||||
\n-> PASSWORD""")
|
|
|
@ -1,2 +0,0 @@
|
||||||
requests-html==0.10.0
|
|
||||||
python-dotenv==0.19.2
|
|
74
src/main.rs
Normal file
74
src/main.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use directories::ProjectDirs;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
mod marks;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
username: String,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[clap(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Your username
|
||||||
|
#[clap(value_parser)]
|
||||||
|
username: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if let Some(proj_dirs) = ProjectDirs::from("com", "anri", "uwm") {
|
||||||
|
let config_dir = proj_dirs.config_dir();
|
||||||
|
|
||||||
|
let file_path = config_dir.join("config.toml");
|
||||||
|
|
||||||
|
let config_file = std::fs::read_to_string(&file_path);
|
||||||
|
|
||||||
|
let mut config = match config_file {
|
||||||
|
Ok(file) => match toml::from_str(&file) {
|
||||||
|
Ok(stored_config) => stored_config,
|
||||||
|
Err(file_error) => {
|
||||||
|
// Error in config file
|
||||||
|
if args.username.is_none()
|
||||||
|
&& file_error.to_string().contains("missing field `username`")
|
||||||
|
{
|
||||||
|
// Show error message if needed
|
||||||
|
println!("Error in config file: {}", file_error);
|
||||||
|
}
|
||||||
|
Config {
|
||||||
|
username: if args.username.is_some() {
|
||||||
|
args.username.unwrap()
|
||||||
|
} else {
|
||||||
|
utils::ask_username()
|
||||||
|
},
|
||||||
|
password: Some(utils::ask_password()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => Config {
|
||||||
|
// No config file, ask for creds
|
||||||
|
username: if args.username.is_some() {
|
||||||
|
args.username.unwrap()
|
||||||
|
} else {
|
||||||
|
// Only ask if nothing has been given in args
|
||||||
|
utils::ask_username()
|
||||||
|
},
|
||||||
|
password: Some(utils::ask_password()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.password.is_none() {
|
||||||
|
config.password = Some(utils::ask_password());
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_agent = format!("uwm/{}", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
marks::get_marks(config.username, config.password.unwrap(), &user_agent).await;
|
||||||
|
}
|
||||||
|
}
|
92
src/marks.rs
Normal file
92
src/marks.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
|
/// Retrieves marks for a user
|
||||||
|
pub async fn get_marks(username: String, password: String, user_agent: &str) {
|
||||||
|
// Login
|
||||||
|
let client = login(username, password, user_agent).await.unwrap();
|
||||||
|
|
||||||
|
// Marks
|
||||||
|
load_marks(&client).await.unwrap();
|
||||||
|
|
||||||
|
println!("Notes *récupérés* !")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login to eP8
|
||||||
|
async fn login(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
user_agent: &str,
|
||||||
|
) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
|
||||||
|
let login_url = "https://cas.univ-paris8.fr/cas/login";
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.cookie_store(true)
|
||||||
|
.user_agent(user_agent)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let login_page = Html::parse_document(&client.get(login_url).send().await?.text().await?);
|
||||||
|
|
||||||
|
// LT
|
||||||
|
let sel_lt = Selector::parse(r#"input[name="lt"]"#).unwrap();
|
||||||
|
let lt = login_page
|
||||||
|
.select(&sel_lt)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.value()
|
||||||
|
.attr("value")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
let sel_execution = Selector::parse(r#"input[name="execution"]"#).unwrap();
|
||||||
|
let execution = login_page
|
||||||
|
.select(&sel_execution)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.value()
|
||||||
|
.attr("value")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let params = [
|
||||||
|
("username", username.as_str()),
|
||||||
|
("password", password.as_str()),
|
||||||
|
("_eventId", "submit"),
|
||||||
|
("submit", "SE CONNECTER"),
|
||||||
|
("lt", lt),
|
||||||
|
("execution", execution),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Login
|
||||||
|
let sel_confirmation = Selector::parse("h2").unwrap();
|
||||||
|
if Html::parse_document(
|
||||||
|
&client
|
||||||
|
.post(login_url)
|
||||||
|
.form(¶ms)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
.select(&sel_confirmation)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.inner_html()
|
||||||
|
.contains("Connexion réussie")
|
||||||
|
{
|
||||||
|
println!("Connexion réussie...");
|
||||||
|
Ok(client)
|
||||||
|
} else {
|
||||||
|
panic!("Connexion échouée : Mauvais identifiants")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load marks from scolarite-etudiant
|
||||||
|
async fn load_marks(client: &reqwest::Client) -> Result<Html, Box<dyn std::error::Error>> {
|
||||||
|
let html = client
|
||||||
|
.get("https://scolarite-etudiant.univ-paris8.fr/mondossierweb/#!notesView")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let document = Html::parse_document(&html.text().await?);
|
||||||
|
|
||||||
|
Ok(document)
|
||||||
|
}
|
32
src/utils.rs
Normal file
32
src/utils.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
/// Ask user for username
|
||||||
|
pub fn ask_username() -> std::string::String {
|
||||||
|
let mut user_input = String::new();
|
||||||
|
let stdin = io::stdin();
|
||||||
|
|
||||||
|
print!("Username: ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
stdin.read_line(&mut user_input).unwrap();
|
||||||
|
|
||||||
|
user_input.trim_end().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask user for password
|
||||||
|
pub fn ask_password() -> std::string::String {
|
||||||
|
print!("Password: ");
|
||||||
|
io::stdout().flush().unwrap();
|
||||||
|
rpassword::read_password().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Write a document to a file
|
||||||
|
///
|
||||||
|
/// `html_data` may be created with something like that:
|
||||||
|
/// ```
|
||||||
|
/// Html::parse_document(&client.get("URL").send().await?.text().await?).html()
|
||||||
|
/// ```
|
||||||
|
pub fn write_html(html_data: String) {
|
||||||
|
let mut f = std::fs::File::create("./target/temp.html").unwrap();
|
||||||
|
write!(&mut f, "{}", html_data).unwrap();
|
||||||
|
}
|
Reference in a new issue