Skip to content

Commit

Permalink
Merge pull request #21 from konnectors/feat/upgradePawnote
Browse files Browse the repository at this point in the history
feat: Clisk/server konnector (SCR-845)
  • Loading branch information
doubleface authored Nov 14, 2024
2 parents 2b48bf8 + b10c354 commit 078cc7a
Show file tree
Hide file tree
Showing 19 changed files with 561 additions and 390 deletions.
31 changes: 2 additions & 29 deletions manifest.konnector
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,8 @@
"categories": [
"education"
],
"fields": {
"pronote_url": {
"type": "text"
},
"login": {
"type": "text"
},
"password": {
"type": "password"
}
},
"fields": {},
"clientSide": true,
"folders": [
{
"defaultDir": "$administrative/$konnector/$account"
Expand All @@ -32,7 +23,6 @@
"event",
"document"
],
"screenshots": [],
"permissions": {
"files": {
"type": "io.cozy.files"
Expand All @@ -57,17 +47,8 @@
"langs": [
"fr"
],
"frequency": "daily",
"locales": {
"fr": {
"fields": {
"login": {
"label": "Identifiant de l'élève"
},
"pronote_url": {
"label": "URL PRONOTE de l'établissement"
}
},
"short_description": "Accédez à l'ensemble de vos données scolaires, et conservez-les pour toujours.",
"long_description": "Ce connecteur **sauvegarde vos données d'élève** depuis le service Pronote à l'aide de vos identifiants d'élève et les sécurise **pour votre usage personnel immédiat ou futur.**\n\n<br> Vous pourrez visualiser ces données **avec votre mobile ou votre ordinateur**, dans votre Cozy, notamment au travers d'applications comme Drive, Papillon ou MesPapiers.\n\n<br> Vos données suivantes sont ainsi regroupées et **sauvegardées automatiquement** :\n\n- Emploi du temps\n- Devoirs & travaux rendus\n- Notes et corrections\n- Bulletins scolaires\n- Notifications de retard ou d'absence\n- ... Et même votre inénarrable photo de profil 🙂 !",
"permissions": {
Expand All @@ -89,14 +70,6 @@
}
},
"en": {
"fields": {
"login": {
"label": "Student identifier"
},
"pronote_url": {
"label": "School's Pronote URL "
}
},
"short_description": "Access all your school data and keep it forever.",
"long_description": "This connector **saves your student data** from the Pronote service using your student identifiers and secures it **for your immediate or future personal use.**\n\n<br> You can view these data **on your mobile or computer**, in your Cozy, particularly through applications like Drive, Papillon, or MesPapiers.\n\n<br> These data are **automatically gathered:**\n\n- Timetable\n- Homework & assignments submitted\n- Grades and corrections\n- School report cards\n- Notifications of delay or absence\n- ... And even your unforgettable profile picture 🙂!",
"permissions": {
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "cozy-konnector-pronote",
"name": "pronote",
"version": "2.10.0",
"description": "",
"repository": {
Expand Down Expand Up @@ -27,20 +27,23 @@
"start": "node ./src/index.js",
"dev": "cozy-konnector-dev",
"standalone": "cozy-konnector-standalone",
"pretest": "npm run clean",
"clean": "rm -rf ./data",
"build": "webpack",
"build:client": "webpack --config ./webpack.config.client.js",
"build:server": "webpack --config ./webpack.config.server.js",
"build": "yarn build:client && yarn build:server",
"lint": "eslint --fix .",
"deploy": "git-directory-deploy --directory build/ --branch ${DEPLOY_BRANCH:-build} --repo=${DEPLOY_REPOSITORY:-$npm_package_repository_url}",
"cozyPublish": "cozy-app-publish --token $REGISTRY_TOKEN --build-commit $(git rev-parse ${DEPLOY_BRANCH:-build})",
"travisDeployKey": "./bin/generate_travis_deploy_key"
},
"dependencies": {
"@cozy/minilog": "1.0.0",
"cozy-clisk": "^0.38.0",
"cozy-client": "^48.8.0",
"cozy-flags": "^4.0.0",
"cozy-konnector-libs": "5.11.0",
"globals": "^15.8.0",
"pawnote": "^0.22.1",
"pawnote": "^1.2.2",
"pronote-api-maintained": "^3.1.0"
},
"devDependencies": {
Expand Down
208 changes: 208 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { ContentScript } from 'cozy-clisk/dist/contentscript'
import Minilog from '@cozy/minilog'
import waitFor, { TimeoutError } from 'p-wait-for'
const log = Minilog('ContentScript')
Minilog.enable()

const UUID = uuid()

monkeyPatch(UUID)

class PronoteContentScript extends ContentScript {
async ensureAuthenticated({ account, trigger }) {
this.log('info', '🤖 ensureAuthenticated')
const isLastJobError =
trigger?.current_state?.last_failure >
trigger?.current_state?.last_success
this.log('debug', 'isLastJobError: ' + isLastJobError)
const lastJobError = trigger?.current_state?.last_error
this.log('debug', 'lastJobError: ' + lastJobError)

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
} else {
this.store = account?.data
}

return true
}

async ensureNotAuthenticated() {
// always true in incognito mode
return true
}

async getUserDataFromWebsite() {
this.log('info', '🤖 getUserDataFromWebsite')
return {
sourceAccountIdentifier: this.store.login
}
}

async fetch({ account }) {
this.log('info', '🤖 fetch')
if (!this.bridge) {
throw new Error(
'No bridge is defined, you should call ContentScript.init before using this method'
)
}

await this.bridge.call('saveAccountData', this.store)
const jobResult = await this.bridge.call('runServerJob', {
mode: 'pronote-server',
account: account._id
})
if (jobResult.error) {
throw new Error(jobResult.error)
}
}

async waitForLoginState() {
this.log('debug', '🔧 waitForLoginState')
await waitFor(
() => {
return Boolean(window.loginState)
},
{
interval: 1000,
timeout: {
milliseconds: 60 * 1000,
message: new TimeoutError(
`waitForLoginState timed out after ${60 * 1000}ms`
)
}
}
)
return true
}
}

const connector = new PronoteContentScript()
connector
.init({ additionalExposedMethodsNames: ['waitForLoginState'] })
.catch(err => {
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>
<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>
</div>
<div class="btn-contain">
<button id="submitButton" class="themeBoutonPrimaire ieBouton ie-ripple NoWrap ieBoutonDefautSansImage AvecMain">Envoyer</button>
</div>
</fieldset>
`

function cleanURL(url) {
let pronoteURL = url
if (
!pronoteURL.startsWith('https://') &&
!pronoteURL.startsWith('http://')
) {
pronoteURL = `https://${pronoteURL}`
}

pronoteURL = new URL(pronoteURL)
// Clean any unwanted data from URL.
pronoteURL = new URL(
`${pronoteURL.protocol}//${pronoteURL.host}${pronoteURL.pathname}`
)

// Clear the last path if we're not main selection menu.
const paths = pronoteURL.pathname.split('/')
if (paths[paths.length - 1].includes('.html')) {
paths.pop()
}

// Rebuild URL with cleaned paths.
pronoteURL.pathname = paths.join('/')

// Return rebuilt URL without trailing slash.
return pronoteURL.href.endsWith('/')
? 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)
})
})
}

function monkeyPatch(uuid) {
window.hookAccesDepuisAppli = function () {
this.passerEnModeValidationAppliMobile('', uuid)
}
}

function uuid() {
let dateTime = new Date().getTime()

const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (dateTime + Math.random() * 16) % 16 | 0
dateTime = Math.floor(dateTime / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})

return uuid
}
24 changes: 10 additions & 14 deletions src/cozy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,26 @@ const identity = require('./pronote/identity')
const timetable = require('./pronote/timetable')
const homeworks = require('./pronote/homeworks')
const grades = require('./pronote/grades')
const presence = require('./pronote/presence')
// const presence = require('./pronote/presence')
const { log } = require('cozy-konnector-libs')

const handlers = {
identity,
timetable,
homeworks,
grades,
presence
grades
// presence
}

async function cozy_save(type, pronote, fields, options = {}) {
try {
log('info', `🔁 Saving ${type}`)
async function cozy_save(type, session, fields, options = {}) {
log('info', `🔁 Saving ${type}`)

const handler = handlers[type]
if (handler) {
return handler(pronote, fields, options)
}

return false
} catch (err) {
throw new Error(err.message)
const handler = handlers[type]
if (handler) {
return handler(session, fields, options)
}

return false
}

module.exports = {
Expand Down
Loading

0 comments on commit 078cc7a

Please sign in to comment.