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
|
||||
|
||||
[![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
|
||||
## Utilisation
|
||||
|
||||
```
|
||||
$ 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`
|
||||
|
||||
```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? -->
|
||||
Vous pouvez aussi utilisez un fichier `.env` (recommandé) avec : `URL`, `LOGIN` et `PASSWORD` comme nom de variables.
|
||||
|
||||
---
|
||||
|
||||
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