├── frontend
├── script.js
├── css
│ └── index.css
├── logo.png
└── style.css
├── tasks
├── average.js
├── subject.js
└── login.js
├── .gitignore
├── .gitattributes
├── images
├── demo.png
└── error.png
├── model.js
├── router.js
├── views
├── pages
│ ├── about.ejs
│ ├── connect.ejs
│ └── index.ejs
└── partials
│ ├── form.ejs
│ └── navbar.ejs
├── LICENSE
├── package.json
├── server.js
├── README.md
└── bot.js
/frontend/script.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tasks/average.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tasks/subject.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
--------------------------------------------------------------------------------
/frontend/css/index.css:
--------------------------------------------------------------------------------
1 | main{
2 | width:100%;
3 | padding:5rem;
4 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/images/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spieredd/PRONOTE-Analytics/HEAD/images/demo.png
--------------------------------------------------------------------------------
/frontend/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spieredd/PRONOTE-Analytics/HEAD/frontend/logo.png
--------------------------------------------------------------------------------
/images/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spieredd/PRONOTE-Analytics/HEAD/images/error.png
--------------------------------------------------------------------------------
/model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | let user_average = new Schema(
6 | {
7 | user: {
8 | type: String
9 | },
10 | average: {
11 | type: Number
12 | },
13 | date: {
14 | type: Date
15 | }
16 | }
17 | );
18 |
19 | module.exports = mongoose.model("user_average_database", user_average);
--------------------------------------------------------------------------------
/router.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | router.get('/',(req,res) => {
5 | res.status(200).render('pages/index');
6 | });
7 |
8 | router.get('/about', (req,res) => {
9 | res.status(200).render('pages/about');
10 | });
11 |
12 | router.get('/connect',(req,res)=>{
13 | res.status(200).render('pages/connect');
14 | });
15 |
16 | // router
17 | // .use((req, res) => {
18 | // res.status(404);
19 | // res.json({
20 | // error: "Page not found"
21 | // });
22 | // });
23 |
24 | module.exports = router;
--------------------------------------------------------------------------------
/tasks/login.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const chalk = require('chalk');
4 |
5 | const puppeteer = require('puppeteer-extra');
6 |
7 | const StealthPlugin = require('puppeteer-extra-plugin-stealth');
8 | puppeteer.use(StealthPlugin());
9 |
10 | puppeteer.use(require('puppeteer-extra-plugin-anonymize-ua')());
11 |
12 | const login = async(page, username, password) => {
13 | await page.type('input[type=text]', username);
14 | await page.type('input[type=password]', password);
15 |
16 | await page.click('#id_39');
17 |
18 | console.log(chalk.greenBright(`Logged in with ${username} account...`));
19 | }
20 |
21 | module.exports = login;
--------------------------------------------------------------------------------
/views/pages/about.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Pronote Analytics
7 |
8 |
9 |
10 |
11 |
12 | <%- include('../partials/navbar'); %>
13 | About page
14 | Pronote Analytics is a service that lets you
15 |
16 |
--------------------------------------------------------------------------------
/views/pages/connect.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Pronote Analytics
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <%- include('../partials/form'); %>
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/frontend/style.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | html,
4 | body {
5 | height: 100%;
6 | background-color: #e3e3e3;
7 | }
8 |
9 | .main {
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | padding-top: 40px;
14 | padding-bottom: 40px;
15 | }
16 |
17 | .form-signin {
18 | width: 100%;
19 | max-width: 330px;
20 | padding: 15px;
21 | margin: auto;
22 | }
23 |
24 | .form-signin form{
25 | display:flex;
26 | flex-direction: column;
27 | align-items: center;
28 | }
29 |
30 |
31 | .form-signin .checkbox {
32 | font-weight: 400;
33 | }
34 |
35 | .form-signin .form-control {
36 | position: relative;
37 | box-sizing: border-box;
38 | padding: 10px;
39 | font-size: 16px;
40 | }
41 |
42 | .form-signin .form-floating{
43 | width:100%;
44 | }
45 |
46 | button, input{
47 | border:none;
48 | }
49 |
50 | button.btn{
51 | background-color: #c70039;
52 | border:none;
53 | color:#fff;
54 | }
55 |
56 | button.btn:hover{
57 | background-color: #a50432;
58 | color:#fff;
59 | }
60 |
61 |
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 science-math-guy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "PRONOTE-API--unofficial",
3 | "version": "1.0.0",
4 | "description": "PRONOTE unofficial API",
5 | "main": "server.js",
6 | "scripts": {
7 | "devStart": "nodemon server.js",
8 | "start": "node server.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/science-math-guy/PRONOTE-API--unofficial.git"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "bugs": {
18 | "url": "https://github.com/science-math-guy/PRONOTE-API--unofficial/issues"
19 | },
20 | "homepage": "https://github.com/science-math-guy/PRONOTE-API--unofficial#readme",
21 | "dependencies": {
22 | "chalk": "^4.1.0",
23 | "dotenv": "^8.2.0",
24 | "ejs": "^3.1.6",
25 | "express": "^4.17.1",
26 | "mongoose": "^5.11.18",
27 | "puppeteer": "^5.5.0",
28 | "puppeteer-extra": "^3.1.16",
29 | "puppeteer-extra-plugin-anonymize-ua": "^2.2.15",
30 | "puppeteer-extra-plugin-stealth": "^2.6.6"
31 | },
32 | "devDependencies": {
33 | "nodemon": "^2.0.7"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/views/pages/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Pronote Analytics
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%- include('../partials/navbar'); %>
14 |
15 | Pronote Analytics (unofficial)
16 | API pour le logiciel Pronote fait avec NodeJS + PuppeteerJS + MongoDB.
17 | Essayer gratuitement
18 |
19 |
20 |
--------------------------------------------------------------------------------
/views/partials/form.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/partials/navbar.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const chalk = require('chalk');
3 | const router = require('./router');
4 | const mongoose = require('mongoose');
5 | const user_average_database = require("./model");
6 |
7 | const app = express();
8 |
9 | app.use(express.static('frontend'));
10 | app.set('view engine', 'ejs');
11 | app.use(express.json());
12 | app.use(express.urlencoded());
13 | app.use(router);
14 |
15 | const get_user_data = require('./bot');
16 |
17 | const PORT = process.env.PORT || 3000;
18 |
19 | let uri = `mongodb+srv://admin:${process.env.DATABASE_PASSWORD}@cluster0.gnlov.mongodb.net/myFirstDatabase?retryWrites=true&w=majority`;
20 |
21 | mongoose.connect(uri,{
22 | useUnifiedTopology: true,
23 | useNewUrlParser: true
24 | });
25 |
26 | const connection = mongoose.connection;
27 |
28 | connection.once("open", () => {
29 | console.log("MongoDB database connection established successfully");
30 | });
31 |
32 | app.get('/api', (req, res) => {
33 | // let username = process.env.PRONOTE_USERNAME;
34 | // let password = process.env.PRONOTE_PASSWORD;
35 | // let link = process.env.PRONOTE_LINK;
36 |
37 | const username = req.query.username;
38 | const password = req.query.password;
39 | const link = req.query.link;
40 |
41 | if (username && password && link) {
42 | get_user_data(username, password, link)
43 | .then(user_data => {
44 | res.status(200);
45 | res.json(user_data);
46 | });
47 | } else {
48 | console.log("No parameter")
49 | res.sendFile(__dirname + '/frontend/error.html');
50 | }
51 | });
52 |
53 | app.post('/',(req,res)=>{
54 | const username = req.body.username;
55 | const password = req.body.password;
56 | const link = req.body.link;
57 | if (username && password && link) {
58 | get_user_data(username, password, link)
59 | .then(user_data => {
60 | res.status(200);
61 | res.json(user_data);
62 | });
63 | } else {
64 | console.log("No parameter")
65 | res.sendFile(__dirname + '/frontend/error.html');
66 | }
67 | });
68 |
69 | app.listen(PORT, () => {
70 | console.log(`Server listening on port ${PORT}...`);
71 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PRONOTE-Analytics 📈 📈 📈
2 | ## API non-officiel pour le site Pronote
3 |
4 | ## Comment utiliser l'API ?
5 |
6 | Afin d'utiliser cette API Pronote, faites simplement une
7 | ```GET```
8 | request en incluant dans l'url les paramètres suivant:
9 |
10 | ```https://pronote-api-server.herokuapp.com/?username=&password=&link=```
11 |
12 | Exemple ici (les données sensitives sont cachées par des rectangles rouges):
13 |
14 | 
15 |
16 | Si vous rentrez bien les bon paramètres, une réponse contenant un fichier JSON vous sera renvoyé. Ce dernier possèdera un grand nombre de données (notes, moyennes, punitions, devoirs...).
17 |
18 | Si jamais vous ne rentrez pas tout les paramètres ou de mauvais paramètres alors il devrait s'affichait une page comme celle-ci:
19 |
20 | 
21 |
22 | ## Mes données (mot de passe, identifiant...) sont-ils enregistrées quelque part ?
23 |
24 | Non. Pour l'instant aucune donnée n'est enregistrée. Vous pouvez aller le code de la branche
25 | ```main``` si vous en êtes pas sûr. Cette dernière est en effet directement connectée à notre serveur Heroku.
26 |
27 | Toutefois, il est prévu de sauvegarder le nom d'utilisateur seulement dans une base de donnée dans le but de controler le nombre de requests que chaque utilisateur fait.
28 |
29 | ## Vous ne souhaitez tout de même pas que vos mots de passe et identifiants passent par nos serveurs ? Pas de problème !
30 |
31 | Vous pouvez aussi faire tourner le code sur votre propre machine.
32 |
33 | Pour cela ouvrez votre terminal et clonez notre repertoire en entrant la commande suivante:
34 |
35 | ```git clone https://github.com/science-math-guy/PRONOTE-API.git```
36 |
37 | Afin de faire 'executer' le code, entrez la commande suivante:
38 |
39 | ```node server.js```
40 |
41 | Puis rendez-vous sur votre navigateur et entrez l'url suivante en remplaçant bien les paramètres par les votres:
42 |
43 | ```http://localhost:3000/?username=&password=&link=```
44 |
45 | Le code est donc directement tourné sur votre ordinateur et vos mots de passe et identifiants ne sont donc pas envoyé sur d'autre serveurs.
46 |
47 | ## Allez-vous rajoutez plus de données dans le fichier JSON ?
48 |
49 | Oui. Ce projet de créer une API pour le site web de Pronote est encore récent. Actuellement, l'API est déployé sur la plateforme Salesforce Heroku, toutefois, cela reste un MVC (Minimal Viable Product).
50 |
51 | L'API va donc être énormément updaté pendant ces prochains jours.
52 |
53 | ## Quelles technologies furent-utilisés ?
54 |
55 | ### Langages:
56 |
57 | - JavaScript (NodeJS)
58 | - HTML
59 | - CSS
60 |
61 | ### NPM packages
62 |
63 | - Express
64 | - Dotenv
65 | - Chalk
66 | - Puppeteer
67 | - Puppeteer-extra
68 | - Puppeteer-extra-plugin-stealth
69 | - Puppeteer-extra-plugin-anonymize-ua
70 |
71 | - Nodemon (dev dependency)
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/bot.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const chalk = require('chalk');
4 |
5 | const puppeteer = require('puppeteer-extra');
6 |
7 | const StealthPlugin = require('puppeteer-extra-plugin-stealth');
8 | puppeteer.use(StealthPlugin());
9 |
10 | puppeteer.use(require('puppeteer-extra-plugin-anonymize-ua')());
11 |
12 | const get_user_data = async(username, password, link) => {
13 |
14 | console.log(chalk.yellowBright('API GET method...'));
15 |
16 | const browser = await puppeteer.launch({
17 | headless: true,
18 | args: ['--no-sandbox']
19 | });
20 |
21 | const page = await browser.newPage();
22 |
23 | console.log(chalk.yellow('Browser launched...'));
24 |
25 | await page.goto(link);
26 | await page.waitForSelector('input[type=text]');
27 | await page.waitForSelector('#id_39');
28 |
29 | console.log('Selector appeared');
30 |
31 |
32 | console.log(chalk.greenBright('On pronote...'));
33 |
34 | try {
35 | await login(page, username, password);
36 | } catch (error) {
37 | console.log('error during loging');
38 | throw error;
39 | }
40 |
41 | console.log('logged in');
42 |
43 | //const average = await get_user_average(page, username);
44 | const average=20;
45 |
46 | await page.waitForSelector('#id_107id_64');
47 | await page.click('#id_107id_64');
48 |
49 | await page.waitFor(300);
50 |
51 | const averages = {};
52 | averages['Moyennes Matières Elève'] = await get_user_grades(page, username);
53 |
54 | await browser.close();
55 |
56 | const data = {...average, ...averages };
57 |
58 | return data;
59 | };
60 |
61 | const login = async(page, username, password) => {
62 | await page.type('input[type=text]', username);
63 | await page.type('input[type=password]', password);
64 |
65 | await page.click('#id_39');
66 |
67 | console.log(chalk.greenBright(`Logged in with ${username} account...`));
68 | }
69 |
70 | module.exports = login;
71 |
72 | const get_user_average = async(page, username) => {
73 | console.log(chalk.yellowBright('Starting new process...'));
74 |
75 | await page.waitForSelector('#id_107id_64');
76 | await page.click('#id_107id_64');
77 |
78 | await page.waitFor(300);
79 |
80 | const user_average_element = await page.$x("/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[2]/div/div/div[1]/span/span");
81 | let user_average = await page.evaluate(el => el.textContent, user_average_element[0]);
82 |
83 | const class_average_element = await page.$x('/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[2]/div/div/div[2]/span/span');
84 | let class_average = await page.evaluate(el => el.textContent, class_average_element[0]);
85 |
86 | let user_average_parsed = user_average.substring(0, user_average.length - 4);
87 | let class_average_parsed = class_average.substring(0, user_average.length - 4);
88 |
89 | let average_parsed_object = {};
90 | average_parsed_object['Moyenne Générale Eleve'] = user_average_parsed;
91 | average_parsed_object['Moyenne Génrale Classe'] = class_average_parsed;
92 |
93 | console.log(chalk.yellow('End of process.'));
94 |
95 | return average_parsed_object;
96 | }
97 |
98 | const get_user_grades = async(page, username) => {
99 | console.log(chalk.yellowBright('Starting new process...'));
100 |
101 | await page.waitForXPath('/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[1]/div[2]/div/div/table/tbody/tr[8]/td[2]/div/div/div/div/div/div[2]');
102 |
103 | const table_xpath = await page.$x('/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[1]/div[2]/div/div/table');
104 | const number_of_rows = await page.evaluate(table => table.rows.length, table_xpath[0]);
105 |
106 | let subject_xpath = '';
107 | let subject = '';
108 | let subjects = [];
109 | let position = [];
110 |
111 | for (let i = 1; i <= number_of_rows; i++) {
112 | subject_xpath = await page.$x(`/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[1]/div[2]/div/div/table/tbody/tr[${i}]/td[2]/div/div/div/div/div/div[2]`);
113 | subject = await page.evaluate(el => el.textContent, subject_xpath[0]);
114 | if (subject.includes('/') || subject.includes("Aujourd'hui") || subject.includes("Hier")) {
115 | continue;
116 | } else {
117 | subjects.push(subject);
118 | position.push(i);
119 | i++;
120 | }
121 | }
122 |
123 | let subject_average_grade_xpath = '';
124 | let subject_average_grade = '';
125 | let subject_average_grades = [];
126 |
127 | for (let i = 1; i <= number_of_rows; i++) {
128 | subject_average_grade_xpath = await page.$x(`/html/body/div[4]/div[1]/div[2]/table/tbody/tr/td[1]/div/div/div[2]/div[1]/div[1]/div[2]/div/div/table/tbody/tr[${i}]/td[2]/div/div/div/div/div/div[1]`);
129 | try {
130 | subject_average_grade = await page.evaluate(el => el.textContent, subject_average_grade_xpath[0]);
131 | } catch (e) {
132 | console.log(e);
133 | continue;
134 | }
135 |
136 | if (position.includes(i)) {
137 | subject_average_grades.push(subject_average_grade);
138 | } else {
139 | continue;
140 | }
141 | }
142 |
143 | const subject_average_grades_object = {};
144 |
145 | subjects.forEach((key, i) => subject_average_grades_object[key] = subject_average_grades[i]);
146 |
147 | console.log(chalk.yellow('End of process.'));
148 |
149 | return subject_average_grades_object;
150 | }
151 |
152 |
153 |
154 | module.exports = get_user_data;
--------------------------------------------------------------------------------