Compare commits
No commits in common. "main" and "python" have entirely different histories.
9 changed files with 117 additions and 2114 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
@ -1 +1,9 @@
|
||||||
/target
|
/.vscode
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
*.html
|
||||||
|
|
||||||
|
bin/
|
||||||
|
lib/
|
||||||
|
pyvenv.cfg
|
||||||
|
|
1874
Cargo.lock
generated
1874
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
@ -1,19 +0,0 @@
|
||||||
[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,30 +1,15 @@
|
||||||
# UWM
|
# Wrapper de notes pour [uPortal](https://github.com/uPortal-Project/uPortal-start)
|
||||||
|
|
||||||
Récupère les notes depuis le site de Paris 8
|
## Utilisation
|
||||||
|
|
||||||
[![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
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ uwm username
|
python3 main.py "<lien-vers-l'instance-CAS-pour-la-connexion-à-uPortal>" "<pseudo>" "<mot-de-passe>"
|
||||||
|
|
||||||
|
# Exemple URL : https://cas.XXX.xxx/cas/login?service=https://e-p8.XXX.xxx/uPortal/Login
|
||||||
```
|
```
|
||||||
|
|
||||||
### Avec un fichier de configuration `config.toml`
|
Vous pouvez aussi utilisez un fichier `.env` (recommandé) avec : `URL`, `LOGIN` et `PASSWORD` comme nom de variables.
|
||||||
|
|
||||||
```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? -->
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Anciennement [uPortalWrapperMarks](https://git.kennel.ml/Anri/uportalWrapperMarks/src/branch/python).
|
Testé avec l'[instance de Paris 8](https://e-p8.univ-paris8.fr).
|
||||||
|
|
99
main.py
Normal file
99
main.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
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""")
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
requests-html==0.10.0
|
||||||
|
python-dotenv==0.19.2
|
74
src/main.rs
74
src/main.rs
|
@ -1,74 +0,0 @@
|
||||||
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
92
src/marks.rs
|
@ -1,92 +0,0 @@
|
||||||
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
32
src/utils.rs
|
@ -1,32 +0,0 @@
|
||||||
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