Skip to content

Commit

Permalink
Merge pull request #22 from konnectors/feat/upgradePawnote
Browse files Browse the repository at this point in the history
feat: Now use pawnote 1.2.2
  • Loading branch information
doubleface authored Nov 20, 2024
2 parents 078cc7a + d5dcf16 commit ddd94ed
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 72 deletions.
273 changes: 204 additions & 69 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* eslint no-constant-condition: off */
/* eslint no-console: off */

import { ContentScript } from 'cozy-clisk/dist/contentscript'
import Minilog from '@cozy/minilog'
import waitFor, { TimeoutError } from 'p-wait-for'
Expand All @@ -8,6 +11,9 @@ const UUID = uuid()

monkeyPatch(UUID)

window.SELECT_STYLE =
'border-bottom: 1px solid var(--theme-foncee); box-shadow: 0 1px 0 0 var(--theme-foncee); width: 100%; padding: .4rem; padding-left: 2rem; font-size: var(--taille-m); margin: 0 0 .25rem 0;'

class PronoteContentScript extends ContentScript {
async ensureAuthenticated({ account, trigger }) {
this.log('info', '🤖 ensureAuthenticated')
Expand All @@ -21,61 +27,72 @@ class PronoteContentScript extends ContentScript {
await this.setWorkerState({ incognito: true })
let url = account?.data?.url
this.log('debug', 'url: ' + url)
if (!url || (isLastJobError && lastJobError === 'LOGIN_FAILED')) {
await this.setWorkerState({ visible: true })
await this.goto(
'https://demo.index-education.net/pronote/mobile.eleve.html'
)
await this.waitForElementInWorker('nav')
url = await this.evaluateInWorker(getUrlFromUser)
await this.setWorkerState({ visible: false })

await this.goto(
url + '/infoMobileApp.json?id=0D264427-EEFC-4810-A9E9-346942A862A4'
)
await new Promise(resolve => window.setTimeout(resolve, 2000))
await this.evaluateInWorker(function (UUID) {
const PRONOTE_COOKIE_EXPIRED = new Date(0).toUTCString()
const PRONOTE_COOKIE_VALIDATION_EXPIRES = new Date(
new Date().getTime() + 5 * 60 * 1000
).toUTCString()
const PRONOTE_COOKIE_LANGUAGE_EXPIRES = new Date(
new Date().getTime() + 365 * 24 * 60 * 60 * 1000
).toUTCString()
const json = JSON.parse(document.body.innerText)
const lJetonCas = !!json && !!json.CAS && json.CAS.jetonCAS
document.cookie = `appliMobile=; expires=${PRONOTE_COOKIE_EXPIRED}`

if (lJetonCas) {
document.cookie = `validationAppliMobile=${lJetonCas}; expires=${PRONOTE_COOKIE_VALIDATION_EXPIRES}`
document.cookie = `uuidAppliMobile=${UUID}; expires=${PRONOTE_COOKIE_VALIDATION_EXPIRES}`
document.cookie = `ielang=1036; expires=${PRONOTE_COOKIE_LANGUAGE_EXPIRES}`
}
}, UUID)
await this.goto(`${url}/mobile.eleve.html?fd=1`)

await this.setWorkerState({ visible: true })
await this.runInWorkerUntilTrue({
method: 'waitForLoginState'
})
await this.setWorkerState({ visible: false })
const loginState = await this.evaluateInWorker(() => window.loginState)

const loginTokenParams = {
url,
kind: 6,
login: loginState.login,
token: loginState.mdp,
deviceUUID: UUID
}
this.store = loginTokenParams
const needsUserAuthenticate =
!url || (isLastJobError && lastJobError === 'LOGIN_FAILED')
if (needsUserAuthenticate) {
await this.userAuthenticate()
} else {
this.store = account?.data
}

return true
}

async requestUrl() {
await this.goto(
'https://demo.index-education.net/pronote/mobile.eleve.html'
)
await this.waitForElementInWorker('nav')
await this.setWorkerState({ visible: true })
const url = await this.evaluateInWorker(getUrlFromUser)
await this.setWorkerState({ visible: false })
return url
}

async userAuthenticate() {
const url = await this.requestUrl()
await this.goto(`${url}/mobile.eleve.html`)
await this.goto(
url + '/infoMobileApp.json?id=0D264427-EEFC-4810-A9E9-346942A862A4'
)
await new Promise(resolve => window.setTimeout(resolve, 2000))
await this.evaluateInWorker(function (UUID) {
const PRONOTE_COOKIE_EXPIRED = new Date(0).toUTCString()
const PRONOTE_COOKIE_VALIDATION_EXPIRES = new Date(
new Date().getTime() + 5 * 60 * 1000
).toUTCString()
const PRONOTE_COOKIE_LANGUAGE_EXPIRES = new Date(
new Date().getTime() + 365 * 24 * 60 * 60 * 1000
).toUTCString()
const json = JSON.parse(document.body.innerText)
const lJetonCas = !!json && !!json.CAS && json.CAS.jetonCAS
document.cookie = `appliMobile=; expires=${PRONOTE_COOKIE_EXPIRED}`

if (lJetonCas) {
document.cookie = `validationAppliMobile=${lJetonCas}; expires=${PRONOTE_COOKIE_VALIDATION_EXPIRES}`
document.cookie = `uuidAppliMobile=${UUID}; expires=${PRONOTE_COOKIE_VALIDATION_EXPIRES}`
document.cookie = `ielang=1036; expires=${PRONOTE_COOKIE_LANGUAGE_EXPIRES}`
}
}, UUID)
await this.goto(`${url}/mobile.eleve.html?fd=1`)

await this.setWorkerState({ visible: true })
await this.runInWorkerUntilTrue({
method: 'waitForLoginState'
})
await this.setWorkerState({ visible: false })
const loginState = await this.evaluateInWorker(() => window.loginState)

const loginTokenParams = {
url,
kind: 6,
login: loginState.login,
token: loginState.mdp,
deviceUUID: UUID
}
this.store = loginTokenParams
}

async ensureNotAuthenticated() {
// always true in incognito mode
return true
Expand Down Expand Up @@ -133,24 +150,132 @@ connector
log.warn(err)
})

function getUrlFromUser() {
document.querySelector('nav').remove()
document.querySelector('form').remove()
document.querySelector('main').innerHTML = `
<fieldset class="login-contain">
<h3 class="logo_pronote">
<span>PRONOTE</span>
</h3>
async function getUrlFromUser() {
window.filterSchools = function () {
window.setTimeout(() => {
// timeout to have the input field updated
const filter = document.querySelector('#filter').value
if (filter?.length > 0) {
document.querySelector('#schools').innerHTML = window.schoolPropositions
.filter(school => school.nomEtab.includes(filter.toUpperCase()))
.map(
school =>
`<option value="${school.url}">${school.nomEtab}</options>`
)
.join('\n')
} else {
document.querySelector('#schools').innerHTML = window.schoolPropositions
.map(
school =>
`<option value="${school.url}">${school.nomEtab}</options>`
)
.join('\n')
}
}, 10)
}
async function getSelectedSchool(schools) {
let schoolsTemplate = `
<label for="filter">Sélectionnez un établissement</label><br>
<input onchange="window.filterSchools()" onkeydown="window.filterSchools()" id="filter" type="text" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" class="full-width" />
<select style="${window.SELECT_STYLE}" id="schools" name="schools" size="5" class="full-width">`
schoolsTemplate += schools
.map(school => `<option value="${school.url}">${school.nomEtab}</option>`)
.join('\n')
schoolsTemplate += `</select>`
return getValues(
schoolsTemplate,
() => document.querySelector('#schools').value
)
}
async function getSchoolPropositions(city) {
const response = await fetch(
'https://www.index-education.com/swie/geoloc.php',
{
method: 'POST',
body: `data={"nomFonction":"geoLoc","lat":"${city.latitude}","long":"${city.longitude}"}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
}
)
return await response.json()
}
async function getSelectedCity(cities) {
let citiesTemplate = `<label for="cities" class="">Sélectionnez une ville</label><br>
<select style="${window.SELECT_STYLE}" id="cities" name="cities" size="5" class="full-width">`
citiesTemplate += cities
.map(
city =>
`<option value="${city.id}">${city.name} (${city.postCode})</option>`
)
.join('\n')
citiesTemplate += `</select>`
return getValues(citiesTemplate, () =>
cities.find(city => city.id === document.querySelector('#cities').value)
)
}
async function getCityStringOrUrl() {
const template = `
<div class="input-field">
<input id="url" type="text" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" class="full-width" aria-required="true" />
<label for="url" class="icon_uniF2BD active">URL fournie par votre établissement</label>
<input id="url" type="text" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" class="full-width" />
<label for="url" class="">Saisissez directement l'url de votre ENT</label>
</div>
<div class="btn-contain">
<div class="input-field">
<input id="ville" type="text" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" class="full-width" />
<label for="ville" class="">ou cherchez votre établissement par ville</label>
</div>`
return getValues(template, () => ({
city: document.querySelector('#ville').value,
url: document.querySelector('#url').value
}))
}
async function getCitiesPropositions(cityString) {
const response = await fetch(
'https://api-adresse.data.gouv.fr/search/?type=municipality&limit=15&q=' +
cityString,
{
method: 'GET',
mode: 'cors'
}
)
const data = await response.json()
return data.features.map(feature => ({
id: feature.properties.id,
name: feature.properties.city,
postCode: feature.properties.postcode,
latitude: feature.geometry.coordinates[1],
longitude: feature.geometry.coordinates[0]
}))
}
async function getValues(template, getter) {
init()
document.querySelector('#body').innerHTML =
template +
`
<div class="btn-contain">
<button id="cancelButton" class="themeBoutonNeutre ieBouton ie-ripple NoWrap ieBoutonDefautSansImage AvecMain">Recommencer</button>
<button id="submitButton" class="themeBoutonPrimaire ieBouton ie-ripple NoWrap ieBoutonDefautSansImage AvecMain">Envoyer</button>
</div>
</fieldset>
`

`
return new Promise((resolve, reject) => {
document.querySelector('#cancelButton').addEventListener('click', () => {
reject('CANCEL')
})
document.querySelector('#submitButton').addEventListener('click', () => {
resolve(getter())
})
})
}
function init() {
document.querySelector('main').innerHTML = `
<fieldset class="login-contain">
<h3 class="logo_pronote"><span>PRONOTE</span></h3>
<div id="body" />
</fieldset>
`
document.querySelector('nav')?.remove()
document.querySelector('footer')?.remove()
}
function cleanURL(url) {
let pronoteURL = url
if (
Expand Down Expand Up @@ -180,13 +305,23 @@ function getUrlFromUser() {
? pronoteURL.href.slice(0, -1)
: pronoteURL.href
}
return new Promise(resolve => {
const button = document.querySelector('#submitButton')
button.addEventListener('click', () => {
const url = cleanURL(document.querySelector('#url').value)
resolve(url)
})
})

while (true) {
try {
const { url, city } = await getCityStringOrUrl()
if (url) {
return cleanURL(url)
} else if (city) {
const citiesPropositions = await getCitiesPropositions(city)
const selectedCity = await getSelectedCity(citiesPropositions)
window.schoolPropositions = await getSchoolPropositions(selectedCity)
const url = await getSelectedSchool(window.schoolPropositions)
return url
}
} catch (err) {
console.error(`ERROR: ${err.message}`, err)
}
}
}

function monkeyPatch(uuid) {
Expand Down
6 changes: 3 additions & 3 deletions src/cozy/pronote/grades.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async function get_grades(session) {

// Find the subject in the list of all subjects
const subjectIndex = allGrades.findIndex(
item => item.subject.name === subject.name && item.period === period
item => item.subject?.name === subject?.name && item.period === period
)

// If the subject is not yet in the list, add it
Expand Down Expand Up @@ -82,11 +82,11 @@ async function getReports(session) {
try {
const reportURL = await gradebookPDF(session, period)
allReports.push({
period: period.name,
period: period?.name,
url: reportURL
})
} catch (error) {
log('warn', 'Could not fetch report for period:', period.name)
log('warn', 'Could not fetch report for period:', period?.name)
}
}

Expand Down

0 comments on commit ddd94ed

Please sign in to comment.