diff --git a/botTelegram/package-lock.json b/botTelegram/package-lock.json index 2d42730..61f4635 100644 --- a/botTelegram/package-lock.json +++ b/botTelegram/package-lock.json @@ -10,9 +10,10 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", + "ical-generator": "^6.0.1", "puppeteer": "^19.8.5", "random-useragent": "^0.5.0", - "telegraf": "^4.12.2" + "telegraf": "^4.15.3" }, "devDependencies": { "nodemon": "^2.0.22" @@ -79,6 +80,21 @@ } } }, + "node_modules/@telegraf/types": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-6.9.1.tgz", + "integrity": "sha512-bzqwhicZq401T0e09tu8b1KvGfJObPmzKU/iKCT5V466AsAZZWQrBYQ5edbmD1VZuHLEwopoOVY5wPP4HaLtug==" + }, + "node_modules/@touch4it/ical-timezones": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.9.0.tgz", + "integrity": "sha512-UAiZMrFlgMdOIaJDPsKu5S7OecyMLr3GGALJTYkRgHmsHAA/8Ixm1qD09ELP2X7U1lqgrctEgvKj9GzMbczC+g==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", @@ -585,6 +601,56 @@ "node": ">= 6" } }, + "node_modules/ical-generator": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-6.0.1.tgz", + "integrity": "sha512-m0Li239l4xddH+MveodfAWFFrHrT8F3rGmgR0zyWUe0Mg7Q/XxiPssN+cKer3+WSpfFNyhjdAsqalTUivKl/vQ==", + "dependencies": { + "uuid-random": "^1.3.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@touch4it/ical-timezones": ">=1.6.0", + "@types/luxon": ">= 1.26.0", + "@types/mocha": ">= 8.2.1", + "dayjs": ">= 1.10.0", + "luxon": ">= 1.26.0", + "moment": ">= 2.29.0", + "moment-timezone": ">= 0.5.33", + "rrule": ">= 2.6.8" + }, + "peerDependenciesMeta": { + "@touch4it/ical-timezones": { + "optional": true + }, + "@types/luxon": { + "optional": true + }, + "@types/mocha": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-timezone": { + "optional": true + }, + "rrule": { + "optional": true + } + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1166,18 +1232,18 @@ } }, "node_modules/telegraf": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.12.2.tgz", - "integrity": "sha512-PgwqI4wD86cMqVfFtEM9JkGGnMHgvgLJbReZMmwW4z35QeOi4DvbdItONld4bPnYn3A1jcO0SRKs0BXmR+x+Ew==", + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.15.3.tgz", + "integrity": "sha512-pm2ZQAisd0YlUvnq6xdymDfoQR++8wTalw0nfw7Tjy0va+V/0HaBLzM8kMNid8pbbt7GHTU29lEyA5CAAr8AqA==", "dependencies": { + "@telegraf/types": "^6.9.1", "abort-controller": "^3.0.0", "debug": "^4.3.4", "mri": "^1.2.0", "node-fetch": "^2.6.8", "p-timeout": "^4.1.0", "safe-compare": "^1.1.4", - "sandwich-stream": "^2.0.2", - "typegram": "^4.3.0" + "sandwich-stream": "^2.0.2" }, "bin": { "telegraf": "lib/cli.mjs" @@ -1239,11 +1305,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/typegram": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/typegram/-/typegram-4.3.0.tgz", - "integrity": "sha512-pS4STyOZoJ++Mwa9GPMTNjOwEzMkxFfFt1By6IbMOJfheP0utMP/H1ga6J9R4DTjAYBr0UDn4eQg++LpWBvcAg==" - }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -1264,6 +1325,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid-random": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", + "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -1431,6 +1497,18 @@ "yargs": "17.7.1" } }, + "@telegraf/types": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-6.9.1.tgz", + "integrity": "sha512-bzqwhicZq401T0e09tu8b1KvGfJObPmzKU/iKCT5V466AsAZZWQrBYQ5edbmD1VZuHLEwopoOVY5wPP4HaLtug==" + }, + "@touch4it/ical-timezones": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@touch4it/ical-timezones/-/ical-timezones-1.9.0.tgz", + "integrity": "sha512-UAiZMrFlgMdOIaJDPsKu5S7OecyMLr3GGALJTYkRgHmsHAA/8Ixm1qD09ELP2X7U1lqgrctEgvKj9GzMbczC+g==", + "optional": true, + "peer": true + }, "@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", @@ -1797,6 +1875,14 @@ "debug": "4" } }, + "ical-generator": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-6.0.1.tgz", + "integrity": "sha512-m0Li239l4xddH+MveodfAWFFrHrT8F3rGmgR0zyWUe0Mg7Q/XxiPssN+cKer3+WSpfFNyhjdAsqalTUivKl/vQ==", + "requires": { + "uuid-random": "^1.3.2" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2219,18 +2305,18 @@ } }, "telegraf": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.12.2.tgz", - "integrity": "sha512-PgwqI4wD86cMqVfFtEM9JkGGnMHgvgLJbReZMmwW4z35QeOi4DvbdItONld4bPnYn3A1jcO0SRKs0BXmR+x+Ew==", + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.15.3.tgz", + "integrity": "sha512-pm2ZQAisd0YlUvnq6xdymDfoQR++8wTalw0nfw7Tjy0va+V/0HaBLzM8kMNid8pbbt7GHTU29lEyA5CAAr8AqA==", "requires": { + "@telegraf/types": "^6.9.1", "abort-controller": "^3.0.0", "debug": "^4.3.4", "mri": "^1.2.0", "node-fetch": "^2.6.8", "p-timeout": "^4.1.0", "safe-compare": "^1.1.4", - "sandwich-stream": "^2.0.2", - "typegram": "^4.3.0" + "sandwich-stream": "^2.0.2" }, "dependencies": { "node-fetch": { @@ -2271,11 +2357,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "typegram": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/typegram/-/typegram-4.3.0.tgz", - "integrity": "sha512-pS4STyOZoJ++Mwa9GPMTNjOwEzMkxFfFt1By6IbMOJfheP0utMP/H1ga6J9R4DTjAYBr0UDn4eQg++LpWBvcAg==" - }, "unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -2296,6 +2377,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "uuid-random": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", + "integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/botTelegram/package.json b/botTelegram/package.json index 49bf4e3..7a772de 100644 --- a/botTelegram/package.json +++ b/botTelegram/package.json @@ -18,12 +18,13 @@ "homepage": "https://github.com/JGaviria0/BotNotasUTP#readme", "type": "module", "dependencies": { + "ical-generator": "^6.0.1", "dotenv": "^16.3.1", "puppeteer": "^19.8.5", "random-useragent": "^0.5.0", - "telegraf": "^4.12.2" + "telegraf": "^4.15.3" }, "devDependencies": { "nodemon": "^2.0.22" } -} +} \ No newline at end of file diff --git a/botTelegram/src/botTelegram.js b/botTelegram/src/botTelegram.js index d48edf8..7afcdf3 100644 --- a/botTelegram/src/botTelegram.js +++ b/botTelegram/src/botTelegram.js @@ -1,11 +1,13 @@ -import { Telegraf } from "telegraf"; +import { Telegraf, Input } from "telegraf"; import 'dotenv/config'; -import {historicGradesScraping, logInScraping } from "./util/scraper.js" +import fs from 'fs'; import { validateInputLogIn } from "./util/validations.js"; import { readHTML } from "./util/extractValues.js"; +import {historicGradesScraping, logInScraping, getGrades, getSchedule } from "./util/scraper.js" const GRADES_PAGE_URL = "https://app4.utp.edu.co/reportes/ryc/ReporteDetalladoNotasxEstudiante.php"; const HISTORIC_PAGE_URL = "https://app4.utp.edu.co/MatAcad/verificacion/historial-web/programas.php"; +const SCHEDULE_PAGE_URL = "https://app4.utp.edu.co/MatAcad/verificacion/horario.php"; const USERS_ID_DEFAULT_LENGTH = 10; // Amount of numbers of the citizen's id const URL_BOT = process.env.URL_BOT; @@ -17,10 +19,11 @@ bot.use((ctx, next) => { }) bot.start((ctx) => { - ctx.reply('Welcome'); + ctx.reply(`Welcome ${ctx.from.first_name}`); ctx.reply(`Para usar el bot alguno de los siguientes comando\n - /notas cedula contraseña\n - /promedio cedula contraseña\n + /notas código contraseña\n + /promedio código contraseña\n + /horario código contraseña\n Los datos del portal estudiantil`) }) @@ -57,11 +60,11 @@ bot.command([/notas.*/], async (ctx) => { const password = userInput[2]; validateInputLogIn(id, userInput) - const page = await logInScraping(id, password); + const {page} = await logInScraping(id, password); await page.goto(GRADES_PAGE_URL); const values = await readHTML(page); - console.log(values); + // console.log(values); for (const ms of values) { ctx.reply(ms) @@ -86,7 +89,7 @@ bot.command([/promedio.*/], async (ctx) => { const password = userInput[2]; validateInputLogIn(id, userInput) - const page = await logInScraping(id, password); + const {page, browser} = await logInScraping(id, password); await page.goto(HISTORIC_PAGE_URL); const userPrograms = await historicGradesScraping(page); @@ -97,12 +100,31 @@ bot.command([/promedio.*/], async (ctx) => { const programsIds = [] userPrograms.forEach(program => programsIds.push(program.id)); - console.log(programsIds); - bot.command(programsIds, () => { - console.log("Ha seleccionado una carrera"); + // console.log(programsIds); + + bot.command(programsIds, async (ctx) => { + const page = await browser.newPage(); + await page.goto(HISTORIC_PAGE_URL); + const programId = ctx.update.message.text.slice(1); + // console.log(page.isClosed()); + const { grades } = await getGrades(page, programId); + + let sumGrades = 0.0, sumCredits = 0.0; + for (let i = 0; i < grades.length; i++) { + const grade = grades[i].grade; + const credit = grades[i].cred; + + sumGrades += grade * credit; + sumCredits += credit; + } + + const result = sumGrades / sumCredits; + + ctx.reply(`Tu promedio acumulado es de: ${result.toFixed(2)}`) }); - page.close(); + await page.close(); + // await browser.close(); } catch (error) { //ERRORS_HANDLING[error.name](error.message, ctx) console.log(error); @@ -111,4 +133,43 @@ bot.command([/promedio.*/], async (ctx) => { } }) -bot.launch() \ No newline at end of file +// Command to export schedule +bot.command([/horario.*/], async (ctx) => { + showInfoMessage(ctx, 'Vamos a procesar su peticion, esto puede tardar algunos minutos.'); + + try { + const userInput = ctx.update.message.text.split(" "); + const id = userInput[1]; + const password = userInput[2]; + + validateInputLogIn(id, userInput) + const {page, browser} = await logInScraping(id, password); + await page.goto(SCHEDULE_PAGE_URL); + // await page.waitForNavigation(); + try { + await getSchedule(page) + } catch (error) { + console.log("Failed to get schedule") + } + + + if (fs.existsSync('./calendar.ics')){ + ctx.reply('Tu horario se ha creado'); + ctx.replyWithDocument(Input.fromLocalFile('./calendar.ics', 'calendar.ics')).catch((error) => { + console.log(error); + }) + } else { + ctx.reply("Ocurrió un error durante el envío del calendario") + } + + + await page.close(); + await browser.close(); + } catch (error) { + console.log(error); + } finally { + ctx.deleteMessage(ctx.update.message.message_id); + } +}); + +bot.launch() diff --git a/botTelegram/src/util/scraper.js b/botTelegram/src/util/scraper.js index 47fa56a..065b1a3 100644 --- a/botTelegram/src/util/scraper.js +++ b/botTelegram/src/util/scraper.js @@ -1,4 +1,6 @@ -import puppeteer from "puppeteer"; +import puppeteer, { Page } from "puppeteer"; +import {ICalCalendar, ICalAlarmType} from 'ical-generator'; +import { writeFile } from 'node:fs/promises'; import * as randomUseragent from "random-useragent"; import { IncorrectData } from "./errors.js"; @@ -6,15 +8,17 @@ import { IncorrectData } from "./errors.js"; const logInScraping = async (user, password) => { - const header = randomUseragent.getRandom((ua) => { - return ua.browserName == 'Firefox'; - }); - const browser = await puppeteer.launch({ headless: false, ignoreHTTPSErrors: true, }); + //console.log(`La dirección WebSocket es: ${browserWSEndpoint}`); + + const header = randomUseragent.getRandom((ua) => { + return ua.browserName == 'Firefox'; + }); + const page = await browser.newPage(); await page.setUserAgent(header); @@ -36,24 +40,21 @@ const logInScraping = async (user, password) => { if (page.url() != 'https://app4.utp.edu.co/pe/utp.php') throw new IncorrectData("Usuario y/o contrasela incorrectos"); - return page; // returns portal home page (After log in) + return {page, browser}; // returns portal home page (After log in) //await page.goto("https://app4.utp.edu.co/reportes/ryc/ReporteDetalladoNotasxEstudiante.php", {timeout: 0}) } -const goToPage = async (homePage, pageUrl) => { - return await homePage.goto(pageUrl); -} - -const historicGradesScraping = async (programsPage) => { - await programsPage.waitForSelector('#cmbprogramas'); - const userPrograms = await programsPage.evaluate(() => { - var options = Array.from(document.querySelectorAll('html body div#utp-contenedor div#utp-contenido div fieldset.form1line select#cmbprogramas option')); +const historicGradesScraping = async (page) => { + await page.waitForSelector('#cmbprogramas'); + await new Promise(r => setTimeout(r, 600)); + const userPrograms = await page.evaluate(() => { + var options = Array.from(document.querySelectorAll('select#cmbprogramas option')); var finalOptions = []; for(var op of options) { var subjectId = op.textContent.substring(0,2); var subjectName = op.textContent.substring(3); - if(subjectId.match(/^[0-9]+$/) != null){ // Id only has numbers + if(subjectId != "Pr"){ // Id diferent of Programa finalOptions.push({id: subjectId, name: subjectName}); } } @@ -62,4 +63,162 @@ const historicGradesScraping = async (programsPage) => { return userPrograms; } -export { logInScraping, goToPage, historicGradesScraping }; +/** + * getGrades + * @param {Page} page + * @param {String} programId + */ +const getGrades = async (page, programId) => { + // await page.goto("https://app4.utp.edu.co/MatAcad/verificacion/historial-web/programas.php"); + // await page.waitForNavigation(); + const selector = await page.waitForSelector('#cmbprogramas'); + await selector.select(programId); + await new Promise(r => setTimeout(r, 600)); + + + const getGrades = await page.evaluate( () => { + var subjects = Array.from(document.querySelectorAll('fieldset#infoHistorial table.tblNotas table.tblDetalle tr td:nth-child(2)')); + var creds = Array.from(document.querySelectorAll('fieldset#infoHistorial table.tblNotas table.tblDetalle tr td:nth-child(3)')); + var allGrades = Array.from(document.querySelectorAll('fieldset#infoHistorial table.tblNotas table.tblDetalle tr td:nth-child(5)')); + var state = Array.from(document.querySelectorAll('fieldset#infoHistorial table.tblNotas table.tblDetalle tr td:nth-child(7)')); + + var gradesForSubject = {} + var grades = [] + for (var i = 0; i < allGrades.length; i++) { + if (state[i].innerText !== "Cursando") { + var grade = allGrades[i].innerText.replace(/\s/g, '').replace(/,/g, '.').toLowerCase(); + var data = {} + if (grade.includes("aprobado")) grade = 5; + + data.grade = parseFloat(grade, 10); + data.cred = parseInt(creds[i].innerText, 10); + + // It save in an object with grades and credits for subject + gradesForSubject[subjects[i].innerText] = data; + // It save in an array with grades and credits + grades.push(data); + } + } + return {grades, gradesForSubject} + }); + + // browser.close().then(setTimeout(() => console.log("Closing browser"), 600)); + return getGrades +} + +const getSchedule = async (page) => { + await new Promise(r => setTimeout(r, 600)); + + const {info, subjects, teachers} = await page.evaluate( () => { + var scheduleData = Array.from(document.querySelector('div>fieldset.form1line').childNodes); + var info = [], subjects = [], teachers = []; + + for (var elemento of scheduleData){ + if (elemento.nodeName == '#text' && !elemento.textContent.includes('\n')){ + info.push(elemento.textContent.replace(/,\s$/, '').replace(/\sde\s/g,' ').replace(/\sa\s/g,' ')); + } + if (elemento.nodeName == 'STRONG' && !elemento.textContent.includes('@')){ + subjects.push(elemento.innerText.slice(6)); + } + + if (elemento.nodeName == 'STRONG' && elemento.textContent.includes('@')){ + teachers.push(elemento.innerText.slice(3)); + } + } + + for (var i = 0; i < info.length; i++){ + info[i] = info[i].replace(/,\s/g, ' ').split(' ') + } + + return {info, subjects, teachers} + }); + + // Creating the schedule + const calendar = new ICalCalendar({ + name: 'Horario de clases', + prodId: { + company: 'Universidad Tecnológica de Pereira', + product: 'Horario asignado', + language: 'ES' + } + }); + + + for (let i = 0; i < info.length; i++) { + for (let j = 0; j < info[i].length; j+=4) { + const {startDate, endDate} = getSubjectDate(info[i][j+1] ,info[i][j+2], info[i][j+3]) + calendar.createEvent({ + start: startDate, + end: endDate, + summary: subjects[i], + description: teachers[i] + '\n\nAdvice: You would should go to the classroom 10min before', + location: `Edificio ${info[i][j]}`, + repeating: { + freq: 'WEEKLY', + until: new Date(2024, 5, 26, 0, 0, 0, 0), + wkst: 'SU' + }, + alarms: [ + {type: ICalAlarmType.display, trigger: 1200}, + ] + }); + } + } + + + + + try { + await writeFile('./calendar.ics', calendar.toString()); + } catch (error) { + console.log(error); + } + return calendar; +} + +const getSubjectDate = (day, timeInitial, timeEnd) => { + const days = ["Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"] + + let dayOfWeek = ""; + + // Convierte el dia de la semana dado a un número entre 1 y 7 + for (let i = 0; i < 7; i++) { + if (day === days[i]) { + dayOfWeek = i+1; + } + } + + // Crea una fecha inicial + const startDate = new Date(); + + // Encuentra el día de la semana actual + const startDayOfWeek = startDate.getDay(); + + // Calcula la diferencia entre el día de la semana deseado y el día actual. + const dayDifference = dayOfWeek - startDayOfWeek; + + // Calcula el número de días para agregar a la fecha inicial. + const daysToAdd = dayDifference >= 0 ? dayDifference : dayDifference + 7; + + // Agrega los días necesarios para llegar al día de la semana deseado. + startDate.setDate(startDate.getDate() + daysToAdd); + + // It saves hours and minutes like an array + const timeI = timeInitial.split(':'); + const timeE = timeEnd.split(':'); + // Set time + startDate.setHours(parseInt(timeI[0])); + startDate.setMinutes(parseInt(timeI[1])); + startDate.setSeconds(0); + startDate.setMilliseconds(0); + + // console.log(`HORA ${timeI[0]}:`, startDate.getHours()); + + const endDate = new Date(startDate); + endDate.setHours(parseInt(timeE[0])); + endDate.setMinutes(parseInt(timeE[1])); + + return {startDate, endDate} +} + +export { logInScraping, historicGradesScraping, getGrades, getSchedule };