diff --git a/manifest.konnector b/manifest.konnector index 65f6526..8a8f2cf 100644 --- a/manifest.konnector +++ b/manifest.konnector @@ -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" @@ -32,7 +23,6 @@ "event", "document" ], - "screenshots": [], "permissions": { "files": { "type": "io.cozy.files" @@ -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
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
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": { @@ -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
You can view these data **on your mobile or computer**, in your Cozy, particularly through applications like Drive, Papillon, or MesPapiers.\n\n
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": { diff --git a/package.json b/package.json index 26b12e3..df92723 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "cozy-konnector-pronote", + "name": "pronote", "version": "2.10.0", "description": "", "repository": { @@ -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": { diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..5fccbf7 --- /dev/null +++ b/src/client.js @@ -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 = ` +
+

+ PRONOTE +

+
+ + +
+
+ +
+
+` + + 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 +} diff --git a/src/cozy/index.js b/src/cozy/index.js index b063af9..383dd71 100644 --- a/src/cozy/index.js +++ b/src/cozy/index.js @@ -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 = { diff --git a/src/cozy/pronote/grades.js b/src/cozy/pronote/grades.js index 3406ba5..afb3b0f 100644 --- a/src/cozy/pronote/grades.js +++ b/src/cozy/pronote/grades.js @@ -1,4 +1,5 @@ const { saveFiles, updateOrCreate, log } = require('cozy-konnector-libs') +const { gradesOverview: getGradesOverview, gradebookPDF } = require('pawnote') const { DOCTYPE_GRADE, @@ -11,16 +12,16 @@ const findObjectByPronoteString = require('../../utils/format/format_cours_name' const preprocessDoctype = require('../../utils/format/preprocess_doctype') const { queryAllGrades } = require('../../queries') -async function get_grades(pronote) { +async function get_grades(session) { const allGrades = [] // Get all periods (trimesters, semesters, etc.) - const periods = pronote.periods + const periods = session.instance.periods // For each period, get all grades for (const period of periods) { // Get all grades for each period - const gradesOverview = await pronote.getGradesOverview(period) + const gradesOverview = await getGradesOverview(session, period) // For each grade, get the subject and add it to the list for (const grade of gradesOverview.grades) { @@ -46,13 +47,14 @@ async function get_grades(pronote) { } // For each average, get the subject and add it to the list - for (const average of gradesOverview.averages) { + for (const averageKey of ['classAverage', 'overallAveragegradesOverview']) { + const average = gradesOverview[averageKey] // Get the subject of the average - const subject = average.subject + const subject = average?.subject // 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 (subjectIndex === -1) { allGrades.push({ @@ -71,14 +73,14 @@ async function get_grades(pronote) { return allGrades } -async function getReports(pronote) { +async function getReports(session) { const allReports = [] // Get all reports - const reportPeriods = pronote.readPeriodsForGradesReport() - for (const period of reportPeriods) { + const periods = session.instance.periods + for (const period of periods) { try { - const reportURL = await pronote.generateGradesReportPDF(period) + const reportURL = await gradebookPDF(session, period) allReports.push({ period: period.name, url: reportURL @@ -112,7 +114,7 @@ async function saveReports(pronote, fields) { } const data = await saveFiles(filesToDownload, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, qualificationLabel: 'gradebook', // Grade report @@ -122,9 +124,9 @@ async function saveReports(pronote, fields) { return data } -async function createGrades(pronote, fields, options) { +async function createGrades(session, fields, options) { // Get all grades - const grades = await get_grades(pronote, fields, options) + const grades = await get_grades(session, fields, options) const data = [] // Get options @@ -190,7 +192,7 @@ async function createGrades(pronote, fields, options) { }) const data = await saveFiles(filesToDownload, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, qualificationLabel: 'other_work_document', // Given subject @@ -241,7 +243,7 @@ async function createGrades(pronote, fields, options) { }) const data = await saveFiles(filesToDownload, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, qualificationLabel: 'other_work_document', // Corrected subject @@ -292,7 +294,7 @@ async function createGrades(pronote, fields, options) { const scaleMult = 20 / evals[0].value.outOf // Necessary to normalise grades not on /20 avgGrades = evals[0].value.student * scaleMult } else { - avgGrades = grade.averages.student + avgGrades = grade.averages?.student } // Create the doctype @@ -300,13 +302,13 @@ async function createGrades(pronote, fields, options) { subject: processedCoursName, sourceSubject: grade.subject?.name || 'Cours', title: grade.period.name, - startDate: new Date(grade.period.start).toISOString(), - endDate: new Date(grade.period.end).toISOString(), + startDate: new Date(grade.period.startDate).toISOString(), + endDate: new Date(grade.period.endDate).toISOString(), aggregation: { avgGrades: avgGrades, - avgClass: grade.averages.class_average, - maxClass: grade.averages.max, - minClass: grade.averages.min + avgClass: grade.averages?.class_average, + maxClass: grade.averages?.max, + minClass: grade.averages?.min }, series: evals, relationships: @@ -328,45 +330,41 @@ async function createGrades(pronote, fields, options) { return data } -async function init(pronote, fields, options) { - try { - let files = await createGrades(pronote, fields, options) +async function init(session, fields, options) { + let files = await createGrades(session, fields, options) - /* + /* [Strategy] : don't update grades, they stay the same */ - const existing = await queryAllGrades() - - // remove duplicates in files - const filtered = files.filter(file => { - const found = existing.find(item => { - return ( - item.series.length === file.series.length && - item.startDate === file.startDate && - item.subject === file.subject - ) - }) + const existing = await queryAllGrades() - return !found + // remove duplicates in files + const filtered = files.filter(file => { + const found = existing.find(item => { + return ( + item?.series?.length === file?.series?.length && + item?.startDate === file?.startDate && + item?.subject === file?.subject + ) }) - const res = await updateOrCreate( - filtered, - DOCTYPE_GRADE, - ['startDate', 'subject'], - { - sourceAccount: this.accountId, - sourceAccountIdentifier: fields.login - } - ) + return !found + }) - await saveReports(pronote, fields, options) + const res = await updateOrCreate( + filtered, + DOCTYPE_GRADE, + ['startDate', 'subject'], + { + sourceAccount: fields.account, + sourceAccountIdentifier: fields.login + } + ) - return res - } catch (error) { - throw new Error(error) - } + await saveReports(session, fields, options) + + return res } module.exports = init diff --git a/src/cozy/pronote/homeworks.js b/src/cozy/pronote/homeworks.js index b0420d5..3a6dab1 100644 --- a/src/cozy/pronote/homeworks.js +++ b/src/cozy/pronote/homeworks.js @@ -1,4 +1,5 @@ const { saveFiles, updateOrCreate, log } = require('cozy-konnector-libs') +const { resourcesFromIntervals } = require('pawnote') const { DOCTYPE_HOMEWORK, @@ -13,17 +14,17 @@ const { createDates, getIcalDate } = require('../../utils/misc/createDates') const save_resources = require('../../utils/stack/save_resources') const { queryFilesByName, queryHomeworksByDate } = require('../../queries') -async function get_homeworks(pronote, fields, options) { +async function get_homeworks(session, fields, options) { const dates = createDates(options) - const overview = await pronote.getHomeworkForInterval(dates.from, dates.to) + const overview = await resourcesFromIntervals(session, dates.from, dates.to) return { homeworks: overview } } -async function createHomeworks(pronote, fields, options) { - const interval = await get_homeworks(pronote, fields, options) +async function createHomeworks(session, fields, options) { + const interval = await get_homeworks(session, fields, options) const homeworks = interval.homeworks const data = [] @@ -94,7 +95,7 @@ async function createHomeworks(pronote, fields, options) { }) const data = await saveFiles(filesToDownload, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, qualificationLabel: 'other_work_document', // Homework subject @@ -142,74 +143,67 @@ async function createHomeworks(pronote, fields, options) { return data } -async function init(pronote, fields, options) { - try { - let files = await createHomeworks(pronote, fields, options) +async function init(session, fields, options) { + let files = await createHomeworks(session, fields, options) - /* + /* [Strategy] : don't update past homeworks, only update future homeworks */ - // get existing homeworks - const existing = await queryHomeworksByDate( - fields, - options.dateFrom, - options.dateTo - ) - - // remove duplicates in files - const filtered = files.filter(file => { - const found = existing.find(item => { - // if item.cozyMetadata.updatedAt is less than today - const updated = new Date(item.cozyMetadata.updatedAt) - const today = new Date() - today.setHours(0, 0, 0, 0) - const updatedRecently = updated.getTime() > today.getTime() - - // if item is more recent than today - if (new Date(item.dueDate) > new Date()) { - if (!updatedRecently) { - return false - } - } + // get existing homeworks + const existing = await queryHomeworksByDate( + fields, + options.dateFrom, + options.dateTo + ) - return item.label === file.label && item.start === file.start - }) + // remove duplicates in files + const filtered = files.filter(file => { + const found = existing.find(item => { + // if item.cozyMetadata.updatedAt is less than today + const updated = new Date(item.cozyMetadata.updatedAt) + const today = new Date() + today.setHours(0, 0, 0, 0) + const updatedRecently = updated.getTime() > today.getTime() + + // if item is more recent than today + if (new Date(item.dueDate) > new Date()) { + if (!updatedRecently) { + return false + } + } - return !found + return item.label === file.label && item.start === file.start }) - // for existing files, add their _id and _rev - for (const file of filtered) { - const found = existing.find(item => { - return item.label === file.label && item.start === file.start - }) + return !found + }) - if (found) { - file._id = found._id - file._rev = found._rev - } + // for existing files, add their _id and _rev + for (const file of filtered) { + const found = existing.find(item => { + return item.label === file.label && item.start === file.start + }) + + if (found) { + file._id = found._id + file._rev = found._rev } + } - log( - 'info', - `${filtered.length} new homeworks to save out of ${files.length}` - ) - - const res = await updateOrCreate( - filtered, - DOCTYPE_HOMEWORK, - ['start', 'label'], - { - sourceAccount: this.accountId, - sourceAccountIdentifier: fields.login - } - ) + log('info', `${filtered.length} new homeworks to save out of ${files.length}`) - return res - } catch (error) { - throw new Error(error) - } + const res = await updateOrCreate( + filtered, + DOCTYPE_HOMEWORK, + ['start', 'label'], + { + sourceAccount: fields.account, + sourceAccountIdentifier: fields.login + } + ) + + return res } module.exports = init diff --git a/src/cozy/pronote/identity.js b/src/cozy/pronote/identity.js index 03ced8e..a81eb07 100644 --- a/src/cozy/pronote/identity.js +++ b/src/cozy/pronote/identity.js @@ -1,28 +1,26 @@ const { saveFiles, log, saveIdentity } = require('cozy-konnector-libs') +const { account } = require('pawnote') const { PATH_IDENTITY_PROFILE_PIC } = require('../../constants') const extract_pronote_name = require('../../utils/format/extract_pronote_name') const gen_pronoteIdentifier = require('../../utils/format/gen_pronoteIdentifier') -async function createIdentity(pronote, fields) { +async function createIdentity(session, fields) { // Getting personal information - const information = await pronote.getPersonalInformation() + const information = await account(session) // Getting profile picture - const profile_pic = await save_profile_picture(pronote, fields) + const profile_pic = await save_profile_picture(session, fields) // Formatting the JSON - const json = await format_json(pronote, information, profile_pic) + const json = await format_json(session, information, profile_pic) // Returning the identity return json } -async function format_json(pronote, information, profile_pic) { - const etabInfo = pronote.user?.listeInformationsEtablissements['V'][0] - const scAdress = etabInfo['Coordonnees'] - +async function format_json(session, information, profile_pic) { const address = [] if (information.city && information.city.trim() !== '') { @@ -38,39 +36,18 @@ async function format_json(pronote, information, profile_pic) { }) } - if (scAdress && scAdress['LibelleVille']) { - address.push({ - type: 'School', - label: 'work', - city: scAdress['LibelleVille'], - region: scAdress['Province'], - street: scAdress['Adresse1'], - country: scAdress['Pays'], - code: scAdress['CodePostal'], - formattedAddress: - scAdress['Adresse1'] + - ', ' + - scAdress['CodePostal'] + - ' ' + - scAdress['LibelleVille'] + - ', ' + - scAdress['Pays'] - }) - } - - const identifier = gen_pronoteIdentifier(pronote) + const identifier = gen_pronoteIdentifier(session) const identity = { - // _id: genUUID(), source: 'connector', identifier: identifier, contact: { - fullname: pronote.studentName && pronote.studentName, - name: pronote.studentName && extract_pronote_name(pronote.studentName), + fullname: session.user.name && session.user.name, + name: session.user.name && extract_pronote_name(session.user.name), email: information.email && [ { address: information.email, - type: 'Profressional', + type: 'Professional', label: 'work', primary: true } @@ -84,13 +61,13 @@ async function format_json(pronote, information, profile_pic) { } ], address: address, - company: pronote.schoolName, - jobTitle: 'Élève de ' + pronote.studentClass + company: session.user.resources?.[0].establishmentName, + jobTitle: 'Élève de ' + session.user.resources?.[0].className }, student: { ine: information.INE, - class: pronote.studentClass, - school: pronote.schoolName + class: session.user.resources?.[0].className, + school: session.user.resources?.[0].establishmentName }, relationships: profile_pic && profile_pic['_id'] && { @@ -104,20 +81,20 @@ async function format_json(pronote, information, profile_pic) { return identity } -async function save_profile_picture(pronote, fields) { +async function save_profile_picture(session, fields) { log('info', `🖼️ Saving profile picture at ' + ${PATH_IDENTITY_PROFILE_PIC}`) const documents = [ { filename: 'Photo de classe.jpg', - fileurl: pronote.studentProfilePictureURL, + fileurl: session.user.resources[0].profilePicture.url, shouldReplaceFile: false, subPath: PATH_IDENTITY_PROFILE_PIC } ] const files = await saveFiles(documents, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, qualificationLabel: 'identity_photo', // Class photo @@ -128,9 +105,9 @@ async function save_profile_picture(pronote, fields) { return meta || null } -async function init(pronote, fields) { +async function init(session, fields) { try { - let identity = await createIdentity(pronote, fields) + let identity = await createIdentity(session, fields) log('info', '🗣️ Saving identity for ' + identity.identifier) return saveIdentity(identity, fields.login) } catch (error) { diff --git a/src/cozy/pronote/presence.js b/src/cozy/pronote/presence.js index f3da4cc..ed1af4b 100644 --- a/src/cozy/pronote/presence.js +++ b/src/cozy/pronote/presence.js @@ -73,32 +73,28 @@ async function createPresence(pronote, fields, options) { } async function init(pronote, fields, options) { - try { - let files = await createPresence(pronote, fields, options) + let files = await createPresence(pronote, fields, options) - /* + /* [Strategy] : only update events that are NOT justified yet */ - // remove all justified absences - const filtered = files.filter(file => { - return file.xJustified === false - }) + // remove all justified absences + const filtered = files.filter(file => { + return file.xJustified === false + }) - const res = await updateOrCreate( - filtered, - DOCTYPE_ATTENDANCE, - ['label', 'start'], - { - sourceAccount: this.accountId, - sourceAccountIdentifier: fields.login - } - ) + const res = await updateOrCreate( + filtered, + DOCTYPE_ATTENDANCE, + ['label', 'start'], + { + sourceAccount: fields.account, + sourceAccountIdentifier: fields.login + } + ) - return res - } catch (error) { - throw new Error(error) - } + return res } module.exports = init diff --git a/src/cozy/pronote/timetable.js b/src/cozy/pronote/timetable.js index 5b9c672..22bf05f 100644 --- a/src/cozy/pronote/timetable.js +++ b/src/cozy/pronote/timetable.js @@ -1,4 +1,5 @@ const { updateOrCreate, log } = require('cozy-konnector-libs') +const { timetableFromIntervals, parseTimetable } = require('pawnote') const { DOCTYPE_TIMETABLE_LESSON, @@ -13,28 +14,26 @@ const save_resources = require('../../utils/stack/save_resources') const { queryLessonsByDate } = require('../../queries') // Obtains timetable from Pronote -async function get_timetable(pronote, fields, options) { +async function get_timetable(session, fields, options) { // Generate dates if needed (to get the full week or year) const dates = createDates(options) // Send request to get timetable - const overview = await pronote.getTimetableOverviewForInterval( - dates.from, - dates.to - ) + const timetable = await timetableFromIntervals(session, dates.from, dates.to) // Parse timetable response with settings - return overview.parse({ + parseTimetable(session, timetable, { withSuperposedCanceledClasses: false, withCanceledClasses: true, withPlannedClasses: true }) + return timetable } // Process timetable and create doctypes -async function createTimetable(pronote, fields, options) { +async function createTimetable(session, fields, options) { // Get timetable from Pronote - const timetable = await get_timetable(pronote, fields, options) + const timetable = await get_timetable(session, fields, options) // Empty array to store doctypes const data = [] @@ -59,7 +58,8 @@ async function createTimetable(pronote, fields, options) { `[Timetable] : 📕 Content ${shouldGetContent ? 'saved' : 'ignored'}` ) - for (const lesson of timetable) { + for (const lesson of timetable.classes) { + if (lesson.is !== 'lesson') continue // Get the formatted Cozy name const pronoteString = findObjectByPronoteString(lesson.subject?.name) const processedCoursName = pronoteString.label @@ -139,70 +139,66 @@ async function createTimetable(pronote, fields, options) { return data } -async function init(pronote, fields, options) { - try { - const files = await createTimetable(pronote, fields, options) +async function init(session, fields, options) { + const files = await createTimetable(session, fields, options) - /* + /* [Strategy] : don't update past lessons, only update future lessons + don't update lessons that have been updated today Why ? : past lessons never update, only future ones can be edited / cancelled */ - // query all lessons from dateFrom to dateTo - const existing = await queryLessonsByDate( - fields, - options.dateFrom, - options.dateTo - ) - - // filtered contains only events to update - const filtered = files.filter(file => { - // found returns true if the event is already in the database and doesn't need to be updated - const found = existing.find(item => { - // get the last update date - const updated = new Date(item.cozyMetadata.updatedAt) - - // get today's date - const today = new Date() - today.setHours(0, 0, 0, 0) - - // has the item been updated today ? - const updatedRecently = updated.getTime() > today.getTime() - - // if the lesson is in the future, it can be updated - if (new Date(item.start) > new Date()) { - // only update if the lesson has not been updated - if (!updatedRecently) { - // needs an update since it's in the future and hasn't been updated today - return false - } - } + // query all lessons from dateFrom to dateTo + const existing = await queryLessonsByDate( + fields, + options.dateFrom, + options.dateTo + ) - // else, match the label and start date to know if the event is already in the database - return item.label === file.label && item.start === file.start - }) + // filtered contains only events to update + const filtered = files.filter(file => { + // found returns true if the event is already in the database and doesn't need to be updated + const found = existing.find(item => { + // get the last update date + const updated = new Date(item.cozyMetadata.updatedAt) + + // get today's date + const today = new Date() + today.setHours(0, 0, 0, 0) + + // has the item been updated today ? + const updatedRecently = updated.getTime() > today.getTime() + + // if the lesson is in the future, it can be updated + if (new Date(item.start) > new Date()) { + // only update if the lesson has not been updated + if (!updatedRecently) { + // needs an update since it's in the future and hasn't been updated today + return false + } + } - // only return files that are not found or that needs an update (returned false) - return !found + // else, match the label and start date to know if the event is already in the database + return item.label === file.label && item.start === file.start }) - log('info', `${filtered.length} new events to save out of ${files.length}`) + // only return files that are not found or that needs an update (returned false) + return !found + }) - const res = await updateOrCreate( - filtered, - DOCTYPE_TIMETABLE_LESSON, - ['start', 'label'], - { - sourceAccount: this.accountId, - sourceAccountIdentifier: fields.login - } - ) + log('info', `${filtered.length} new events to save out of ${files.length}`) - return res - } catch (error) { - throw new Error(error) - } + const res = await updateOrCreate( + filtered, + DOCTYPE_TIMETABLE_LESSON, + ['start', 'label'], + { + sourceAccount: fields.account, + sourceAccountIdentifier: fields.login + } + ) + + return res } module.exports = init diff --git a/src/fetch/session.js b/src/fetch/session.js index e941041..6456fbd 100644 --- a/src/fetch/session.js +++ b/src/fetch/session.js @@ -1,62 +1,17 @@ // Librairie Pawnote -const { - authenticatePronoteCredentials, - PronoteApiAccountId, - getPronoteInstanceInformation, - defaultPawnoteFetcher -} = require('pawnote') -const uuid = require('../utils/misc/uuid') -const { log } = require('cozy-konnector-libs') -const { ENTPronoteLogin, getCasName } = require('./ENT') +const { loginToken, createSessionHandle } = require('pawnote') // creates a Pawnote session using the provided credentials -async function Pronote({ url, login, password }) { - try { - // Remove everything after /pronote/ in the URL - const newURL = url.split('/pronote')[0] + '/pronote/' - - // Get data from infoMobileApp.json (contains info about the instance including ENT redirection) - const info = await getPronoteInstanceInformation(defaultPawnoteFetcher, { - pronoteURL: newURL - }) - - // Get the URL of the instance (with a trailing slash to add the mobile.eleve.html endpoint) - const pronoteURL = info.pronoteRootURL + '/' - - // Asks instance information to Pawnote to check if it's a Toutatice instance - const casName = await getCasName(info) - if (casName) { - log('debug', `Found a CAS name : ${casName}`) - } - - // Check if the URL uses the login=true parameter (bypasses ENT redirection) - const usesLoginTrue = url.includes('login=true') - - if (casName && !usesLoginTrue) { - // use ENT function to authenticate using retrived tokens - return ENTPronoteLogin({ url: pronoteURL, login, password, casName }) - } - - // creates a Pawnote session using the provided credentials - const pronote = await authenticatePronoteCredentials(pronoteURL, { - // account type (student by default) - accountTypeID: PronoteApiAccountId.Student, - // provided credentials - username: login, - password: password, - // generate a random UUID for the device - deviceUUID: uuid() - }) - - log( - 'info', - `Pronote session created [${pronote.username} : ${pronote.studentName}]` - ) - - return pronote - } catch (error) { - throw new Error(error) - } +async function Pronote({ url, login, token, deviceUUID }) { + const session = createSessionHandle() + const loginResult = await loginToken(session, { + url, + username: login, + token, + deviceUUID, + kind: 6 + }) + return { session, loginResult } } module.exports = { diff --git a/src/index.js b/src/server.js similarity index 68% rename from src/index.js rename to src/server.js index e7263f9..99cf70a 100644 --- a/src/index.js +++ b/src/server.js @@ -17,7 +17,12 @@ module.exports = new BaseKonnector(start) // Variable globale pour savoir si on doit sauvegarder les fichiers const SHOULD_SAVE = true const SHOULD_GET_LESSON_CONTENT = false // ONLY for small requests, sends a request per course to get the content of the lesson -const SAVES = ['timetable', 'homeworks', 'grades', 'presence'] +const SAVES = [ + 'timetable', + 'homeworks', + 'grades' + // 'presence' +] // Fonction start qui va être exportée async function start(fields) { @@ -26,30 +31,45 @@ async function start(fields) { // Initialisation de la session Pronote await this.deactivateAutoSuccessfulLogin() - const pronote = await Pronote({ - url: fields.pronote_url, - login: fields.login, - password: fields.password + const accountData = this.getAccountData() + const { session, loginResult } = await Pronote(accountData) + await this.saveAccountData({ + ...accountData, + token: loginResult.token, + navigatorIdentifier: loginResult.navigatorIdentifier }) await this.notifySuccessfulLogin() - log('info', 'Pronote session initialized successfully : ' + pronote) + log( + 'info', + 'Pronote session initialized successfully : ' + session.instance + ) // Gets school year dates - let dateFrom = new Date(pronote.firstDate) + let dateFrom = new Date(session.instance.firstDate) let dateToday = new Date() - const dateTo = new Date(pronote.lastDate) + const dateTo = new Date(session.instance.lastDate) // Saves user identity - await cozy_save('identity', pronote, fields) + const envFields = JSON.parse(process.env.COZY_FIELDS || '{}') + await cozy_save('identity', session, { + ...fields, + ...accountData, + ...envFields + }) SAVES.forEach(async save => { - await cozy_save(save, pronote, fields, { - dateFrom: SAVES === 'homeworks' ? dateToday : dateFrom, - dateTo: dateTo, - saveFiles: SHOULD_SAVE && true, - getLessonContent: SHOULD_GET_LESSON_CONTENT - }) + await cozy_save( + save, + session, + { ...fields, ...accountData, ...envFields }, + { + dateFrom: SAVES === 'homeworks' ? dateToday : dateFrom, + dateTo: dateTo, + saveFiles: SHOULD_SAVE && true, + getLessonContent: SHOULD_GET_LESSON_CONTENT + } + ) }) } catch (err) { const error = err.toString() diff --git a/src/utils/format/gen_pronoteIdentifier.js b/src/utils/format/gen_pronoteIdentifier.js index 314181a..f533ae0 100644 --- a/src/utils/format/gen_pronoteIdentifier.js +++ b/src/utils/format/gen_pronoteIdentifier.js @@ -1,21 +1,25 @@ -const gen_pronoteIdentifier = pronote => { +const gen_pronoteIdentifier = session => { // Student name - let name = pronote.studentName.toLowerCase().replace(/ /g, '') + let name = session.user.name.toLowerCase().replace(/ /g, '') name = name.normalize('NFD').replace(/[\u0300-\u036f]/g, '') name = name.replace(/[^a-zA-Z0-9]/g, '') // School name - let school = pronote.schoolName.toLowerCase().replace(/ /g, '') + let school = session.user.resources?.[0].establishmentName + .toLowerCase() + .replace(/ /g, '') school = school.normalize('NFD').replace(/[\u0300-\u036f]/g, '') school = school.replace(/[^a-zA-Z0-9]/g, '') // Student class - let studentClass = pronote.studentClass.toLowerCase().replace(/ /g, '') + let studentClass = session.user.resources?.[0].className + .toLowerCase() + .replace(/ /g, '') studentClass = studentClass.normalize('NFD').replace(/[\u0300-\u036f]/g, '') studentClass = studentClass.replace(/[^a-zA-Z0-9]/g, '') // School year end - const end = new Date(pronote.lastDate).getFullYear() + const end = new Date(session.instance.lastDate).getFullYear() // Return the identifier return `${name}-${school}-${studentClass}-${end}` diff --git a/src/utils/format/remove_html.js b/src/utils/format/remove_html.js index d984eb9..273ffe6 100644 --- a/src/utils/format/remove_html.js +++ b/src/utils/format/remove_html.js @@ -1,4 +1,5 @@ const remove_html = entryHtml => { + if (!entryHtml) return entryHtml // Remove HTML tags let html = entryHtml html = html.replace(/<[^>]*>?/gm, '') diff --git a/src/utils/misc/createDates.js b/src/utils/misc/createDates.js index f2edc1b..2c8ba6f 100644 --- a/src/utils/misc/createDates.js +++ b/src/utils/misc/createDates.js @@ -21,6 +21,7 @@ function createDates(options) { } function getIcalDate(date) { + if (!date) return date return ( date.toISOString().replace(/-/g, '').replace(/:/g, '').replace(/\..+/, '') + 'Z' diff --git a/src/utils/stack/save_resources.js b/src/utils/stack/save_resources.js index 36e1475..b2a9dbb 100644 --- a/src/utils/stack/save_resources.js +++ b/src/utils/stack/save_resources.js @@ -79,7 +79,7 @@ URL=${file.url}`.trim() if (filesToDownload.length > 0) { const data = await saveFiles(filesToDownload, fields, { - sourceAccount: this.accountId, + sourceAccount: fields.account, sourceAccountIdentifier: fields.login, concurrency: 3, validateFile: () => true diff --git a/webpack.config.client.js b/webpack.config.client.js new file mode 100644 index 0000000..8eaf8e7 --- /dev/null +++ b/webpack.config.client.js @@ -0,0 +1,9 @@ +var path = require('path') +module.exports = { + ...require('cozy-konnector-build/webpack.config.clisk'), + entry: './src/client.js', + output: { + path: path.join(process.cwd(), 'build'), + filename: 'main.js' + } +} diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index ea392d1..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,19 +0,0 @@ -const CopyPlugin = require('copy-webpack-plugin') -const webpack = require('webpack') - -const config = require('cozy-konnector-build/webpack.config') - -module.exports = { - ...config, - plugins: [ - ...config.plugins, - new webpack.IgnorePlugin({ resourceRegExp: /^canvas$/ }), - new CopyPlugin({ - patterns: [ - { - from: './node_modules/pronote-api-maintained/src/cas/generics/jsencrypt.min.js' - } - ] - }) - ] -} diff --git a/webpack.config.server.js b/webpack.config.server.js new file mode 100644 index 0000000..12075c7 --- /dev/null +++ b/webpack.config.server.js @@ -0,0 +1,16 @@ +const webpack = require('webpack') +const config = require('cozy-konnector-build/webpack.config') +var path = require('path') + +module.exports = { + ...config, + entry: './src/server.js', + output: { + path: path.join(process.cwd(), 'build'), + filename: 'index.js' + }, + plugins: [ + ...config.plugins, + new webpack.IgnorePlugin({ resourceRegExp: /^canvas$/ }) + ] +} diff --git a/yarn.lock b/yarn.lock index 3ef1ba3..80d131b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,7 +1184,7 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@cozy/minilog@1.0.0": +"@cozy/minilog@1.0.0", "@cozy/minilog@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@cozy/minilog/-/minilog-1.0.0.tgz#1acc1aad849261e931e255a5f181b638315f7b84" integrity sha512-IkDHF9CLh0kQeSEVsou59ar/VehvenpbEUjLfwhckJyOUqZnKAWmXy8qrBgMT5Loxr8Xjs2wmMnj0D67wP00eQ== @@ -1308,6 +1308,13 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@literate.ink/utilities@1.0.0-10641118381.1": + version "1.0.0-10641118381.1" + resolved "https://registry.yarnpkg.com/@literate.ink/utilities/-/utilities-1.0.0-10641118381.1.tgz#8fb97ba575652101a545f1aba7c96ddd7dd5b418" + integrity sha512-omhzgwfAjNXqIjt6dmWbU8pm6QfEvNYetykEXYk+vVaJ8oICglvkChxkC6FkF8zJKMM/lFrNHgD42obQ8lwolQ== + dependencies: + set-cookie-parser "^2.7.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2618,6 +2625,20 @@ cozy-client@^48.8.0: sift "^6.0.0" url-search-params-polyfill "^8.0.0" +cozy-clisk@^0.38.0: + version "0.38.1" + resolved "https://registry.yarnpkg.com/cozy-clisk/-/cozy-clisk-0.38.1.tgz#0b42f723ebed7ef32c13cf6eac3a8bc241dc52c1" + integrity sha512-gSBcIKo3XSDRsoDNn4ocEkzj0S2YZIQMfG1HLnacHrAruay6kXmAF3VB3kTRctuSDpY4vjxUG379CS7i5qMwBQ== + dependencies: + "@cozy/minilog" "^1.0.0" + bluebird-retry "^0.11.0" + ky "^0.25.1" + lodash "^4.17.21" + microee "^0.0.6" + p-timeout "^6.0.0" + p-wait-for "^5.0.2" + post-me "^0.4.5" + cozy-device-helper@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cozy-device-helper/-/cozy-device-helper-2.2.1.tgz#d5822afd818919fa871527e6f78b0265fc1e009b" @@ -3926,7 +3947,7 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^2.4.0: +html-entities@^2.5.2: version "2.5.2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== @@ -4466,6 +4487,11 @@ kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== +ky@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" + integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -5016,11 +5042,23 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-timeout@^6.0.0: + version "6.1.3" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.3.tgz#9635160c4e10c7b4c3db45b7d5d26f911d9fd853" + integrity sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw== + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-wait-for@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-5.0.2.tgz#1546a15e64accf1897377cb1507fa4c756fffe96" + integrity sha512-lwx6u1CotQYPVju77R+D0vFomni/AqRfqLmqQ8hekklqZ6gAY9rONh7lBQ0uxWMkC2AuX9b2DVAl8To0NyP1JA== + dependencies: + p-timeout "^6.0.0" + pako@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -5120,15 +5158,15 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pawnote@^0.22.1: - version "0.22.1" - resolved "https://registry.yarnpkg.com/pawnote/-/pawnote-0.22.1.tgz#c9d18933433412a3312ccf3e6950e970f9c57573" - integrity sha512-MvB0yO3KWpzP7j9bHMHzMEkGHrFXMXhrs3cvBO1oQFcbh6IK1pN1EGUvghOJ8hqGiQPht6213ALg/URhhPs+3Q== +pawnote@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/pawnote/-/pawnote-1.2.2.tgz#6cdfb8ef6f178e41140b1fadf9a6c0fee02dc028" + integrity sha512-7Q3noEiVCcUHkbn/cziHLtaZsTCShg364Srlnl1QScqCFwRljpbCqDXtKmFTvWyOZH+9nC2drV/oTr+aZIjCwQ== dependencies: - html-entities "^2.4.0" + "@literate.ink/utilities" "1.0.0-10641118381.1" + html-entities "^2.5.2" node-forge "^1.3.1" pako "^2.1.0" - set-cookie-parser "^2.6.0" peek-readable@^4.1.0: version "4.1.0" @@ -5184,6 +5222,11 @@ polka@^0.5.2: "@polka/url" "^0.5.0" trouter "^2.0.1" +post-me@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/post-me/-/post-me-0.4.5.tgz#6171b721c7b86230c51cfbe48ddea047ef8831ce" + integrity sha512-XgPdktF/2M5jglgVDULr9NUb/QNv3bY3g6RG22iTb5MIMtB07/5FJB5fbVmu5Eaopowc6uZx7K3e7x1shPwnXw== + postcss-selector-parser@^6.0.9: version "6.0.10" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" @@ -5865,10 +5908,10 @@ set-cookie-parser@^2.3.5: resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== -set-cookie-parser@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" - integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== +set-cookie-parser@^2.7.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== setprototypeof@1.1.1: version "1.1.1"