Compare commits

..

No commits in common. "main" and "python" have entirely different histories.
main ... python

9 changed files with 117 additions and 2114 deletions

10
.gitignore vendored
View file

@ -1 +1,9 @@
/target /.vscode
.env
*.html
bin/
lib/
pyvenv.cfg

1874
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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"

View file

@ -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
View 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
View file

@ -0,0 +1,2 @@
requests-html==0.10.0
python-dotenv==0.19.2

View file

@ -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;
}
}

View file

@ -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(&params)
.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)
}

View file

@ -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();
}