From 19d5ae621da8fb8b49f4b5c15bdc8284451d92ee Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Mon, 9 Jan 2023 12:30:59 -0300 Subject: [PATCH 01/11] Adicionado suporte parcial a UNILAB - CE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursos adicionados: Login, logoff, mudar senha, informações sobre conta, dados das materias, notas, noticias, trabalhos de casa e atividades. --- src/account/sigaa-account-factory.ts | 38 +++ src/account/sigaa-account-unilab.ts | 421 ++++++++++++++++++++++++ src/bonds/sigaa-student-bond.ts | 5 +- src/courses/sigaa-course-student.ts | 38 ++- src/session/login/sigaa-login-unilab.ts | 102 ++++++ src/session/sigaa-session.ts | 2 +- src/sigaa-main.ts | 18 + src/sigaa-types.ts | 8 + 8 files changed, 628 insertions(+), 4 deletions(-) create mode 100755 src/account/sigaa-account-unilab.ts create mode 100644 src/session/login/sigaa-login-unilab.ts diff --git a/src/account/sigaa-account-factory.ts b/src/account/sigaa-account-factory.ts index f794dd3..2e9944f 100755 --- a/src/account/sigaa-account-factory.ts +++ b/src/account/sigaa-account-factory.ts @@ -6,6 +6,7 @@ import { Session } from '@session/sigaa-session'; import { Account } from './sigaa-account'; import { SigaaAccountIFSC } from './sigaa-account-ifsc'; import { SigaaAccountUFPB } from './sigaa-account-ufpb'; +import { SigaaAccountUNILAB } from './sigaa-account-unilab'; /** * Abstraction to represent the class that instantiates the account. @@ -36,6 +37,42 @@ export class SigaaAccountFactory implements AccountFactory { * @param page home page of account (page after login). */ async getAccount(page: Page): Promise { + switch (this.session.institution) { + case 'IFSC': + return new SigaaAccountIFSC( + page, + this.http, + this.parser, + this.session, + this.bondFactory + ); + case 'UFPB': + return new SigaaAccountUFPB( + page, + this.http, + this.parser, + this.session, + this.bondFactory + ); + case 'UNILAB': + return new SigaaAccountUNILAB( + page, + this.http, + this.parser, + this.session, + this.bondFactory + ); + default: + return new SigaaAccountIFSC( + page, + this.http, + this.parser, + this.session, + this.bondFactory + ); + } + + /* if (this.session.institution === 'UFPB') { return new SigaaAccountUFPB( page, @@ -53,5 +90,6 @@ export class SigaaAccountFactory implements AccountFactory { this.bondFactory ); } + */ } } diff --git a/src/account/sigaa-account-unilab.ts b/src/account/sigaa-account-unilab.ts new file mode 100755 index 0000000..457eb35 --- /dev/null +++ b/src/account/sigaa-account-unilab.ts @@ -0,0 +1,421 @@ +import { Parser } from '@helpers/sigaa-parser'; +import { HTTP, ProgressCallback } from '@session/sigaa-http'; +import { Session } from '@session/sigaa-session'; +import { LoginStatus } from '../sigaa-types'; +import { URL } from 'url'; +import { BondFactory, BondType } from '@bonds/sigaa-bond-factory'; +import { Page } from '@session/sigaa-page'; +import { Account } from './sigaa-account'; + +/** + * Responsible for representing the user account. + * @category Internal + */ +export class SigaaAccountUNILAB implements Account { + /** + * @param homepage homepage (page after login) of user. + */ + constructor( + homepage: Page, + private http: HTTP, + private parser: Parser, + private session: Session, + private bondFactory: BondFactory + ) { + this.parseHomepage(homepage); + } + + /** + * Error message when the new password chosen does not meet the security requirements of SIGAA. + * It is thrown by the changePassword() method + */ + readonly errorInvalidCredentials = 'SIGAA: Invalid credentials.'; + + /** + * Error message when the old password is not the current password. + * It is thrown by the changePassword() method. + */ + readonly errorInsufficientPasswordComplexity = + 'SIGAA: Insufficent password complexity.'; + + /** + * Student name cache + */ + private _name?: string; + + /** + * Student e-mail cache + */ + private _emails?: string[]; + + /** + * Array of active bonds. + */ + private activeBonds: BondType[] = []; + + /** + * Array of inactive bonds. + */ + private inactiveBonds: BondType[] = []; + + /** + * It is a promise that stores if the page parser has already completed + */ + private pagehomeParsePromise?: Promise; + + /** + * Parse login result page to fill the instance. + * + * @param homepage home page to parse. + */ + private parseHomepage(homepage: Page): void { + //Since the login page can vary, we should check the type of page. + if ( + homepage.bodyDecoded.includes( + 'O sistema comportou-se de forma inesperada' + ) + ) { + throw new Error( + 'SIGAA: Invalid homepage, the system behaved unexpectedly.' + ); + } + if (homepage.url.href.includes('/portais/discente/discente.jsf')) { + //If it is home page student of desktop version. + this.pagehomeParsePromise = this.parseStudentHomePage(homepage); + } else if ( + homepage.url.href.includes('/sigaa/vinculos.jsf') || + homepage.url.href.includes('/sigaa/escolhaVinculo.do') + ) { + //If it is bond page. + this.pagehomeParsePromise = this.parseBondPage(homepage); + } else if ( + homepage.url.href.includes('/sigaa/telasPosSelecaoVinculos.jsf') + ) { + this.pagehomeParsePromise = this.http + .get(homepage.url.href, { noCache: true }) + .then((page) => this.parseBondPage(page)); + } else { + throw new Error('SIGAA: Unknown homepage format.'); + } + } + + /** + * Parse bond page. + * @param page page to parse. + */ + private async parseBondPage(page: Page) { + const rows = page.$('table.subFormulario tbody tr').toArray(); + for (const row of rows) { + const cells = page.$(row).find('td').toArray(); + if (cells.length === 0) continue; + + const bondType = this.parser.removeTagsHtml( + page.$(row).find('#tdTipo').html() + ); + const status = this.parser.removeTagsHtml(page.$(cells[3]).html()); + let bond; + switch (bondType) { + case 'Discente': { + const registration = this.parser.removeTagsHtml( + page.$(cells[2]).html() + ); + + const url = page.$(row).find('a[href]').attr('href'); + if (!url) + throw new Error('SIGAA: Bond switch url could not be found.'); + const bondSwitchUrl = new URL(url, page.url); + + const program = this.parser + .removeTagsHtml(page.$(cells[4]).html()) + .replace(/^Curso: /g, ''); + bond = this.bondFactory.createStudentBond( + registration, + program, + bondSwitchUrl + ); + break; + } + case 'Docente': { + bond = this.bondFactory.createTeacherBond(); + break; + } + } + if (bond) + if (status === 'Sim') { + this.activeBonds.push(bond); + } else if (status === 'Não') { + this.inactiveBonds.push(bond); + } else { + console.log('SIGAA: WARNING invalid status: ' + status); + } + } + } + + /** + * @inheritdoc + */ + async getActiveBonds(): Promise { + await this.pagehomeParsePromise; + return this.activeBonds; + } + + /** + * @inheritdoc + */ + async getInactiveBonds(): Promise { + await this.pagehomeParsePromise; + return this.inactiveBonds; + } + + /** + * Parse desktop version of student home page page. + */ + private async parseStudentHomePage(homepage: Page) { + const rows = homepage.$('#perfil-docente table').eq(0).find('tr').toArray(); + let registration; + let program; + let status; + + const buttonSwitchBond = this.parser.removeTagsHtml( + homepage.$('#info-usuario p i small a').html() + ); + if (buttonSwitchBond === 'Alterar vínculo') { + const bondPage = await this.http.get('/sigaa/vinculos.jsf'); + return this.parseBondPage(bondPage); + } + + for (const row of rows) { + const cells = homepage.$(row).find('td'); + if (cells.length !== 2) { + throw new Error('SIGAA: Invalid student details page.'); + } + const rowName = this.parser.removeTagsHtml(cells.eq(0).html()); + switch (rowName) { + case 'Matrícula:': + registration = this.parser.removeTagsHtml(cells.eq(1).html()); + break; + case 'Curso:': + program = this.parser + .removeTagsHtml(cells.eq(1).html()) + .replace(/ - (M|T|N)$/g, ''); // Remove schedule letter + break; + case 'Status:': + status = this.parser.removeTagsHtml(cells.eq(1).html()); + } + if (registration && program && status) break; + } + + if (!registration) + throw new Error('SIGAA: Student bond without registration code.'); + + if (!program) throw new Error('SIGAA: Student bond program not found.'); + + if (!status) throw new Error('SIGAA: Student bond status not found.'); + if (status === 'ATIVO' || status === 'INVATIVO') + this.activeBonds.push( + this.bondFactory.createStudentBond(registration, program, null) + ); + else + this.inactiveBonds.push( + this.bondFactory.createStudentBond(registration, program, null) + ); + } + + /** + * @inheritdoc + */ + logoff(): Promise { + return this.http + .get('/sigaa/logar.do?dispatch=logOff') + .then((page) => { + return this.http.followAllRedirect(page); + }) + .then((page) => { + if (page.statusCode !== 200) { + throw new Error('SIGAA: Invalid status code in logoff page.'); + } + this.session.loginStatus = LoginStatus.Unauthenticated; + this.http.closeSession(); + }); + } + + /** + * Get profile picture URL. + * @retuns Picture url or null if the user has no photo. + */ + async getProfilePictureURL(): Promise { + const page = await this.http.get('/sigaa/portais/discente/discente.jsf'); + + const pictureElement = page.$('div[class="foto"] img'); + if (pictureElement.length === 0) return null; + const pictureSrc = pictureElement.attr('src'); + if (!pictureSrc || pictureSrc.includes('/sigaa/img/no_picture.png')) + return null; + return new URL(pictureSrc, page.url); + } + + /** + * Download profile url and save in basepath. + * @param destpath It can be a folder or a file name, if it is a directory then it will be saved inside the folder, if it is a file name it will be saved exactly in this place, but if the folder does not exist it will throw an error. + * @param callback To know the progress of the download, each downloaded part will be called informing how much has already been downloaded. + * @retuns Full path of the downloaded file, useful if the destpath is a directory, or null if the user has no photo. + */ + async downloadProfilePicture( + destpath: string, + callback?: ProgressCallback + ): Promise { + const pictureURL = await this.getProfilePictureURL(); + if (!pictureURL) return null; + return this.http.downloadFileByGet(pictureURL.href, destpath, callback); + } + + /** + * @inheritdoc + */ + async getName(): Promise { + if (this._name) return this._name; + const page = await this.http.get('/sigaa/portais/discente/discente.jsf'); + if (page.statusCode === 200) { + const username = this.parser.removeTagsHtml( + page.$('p.usuario > span').html() + ); + this._name = username; + return username; + } else { + throw new Error('SIGAA: Unexpected status code at student profile page.'); + } + } + + /** + * @inheritdoc + */ + async getEmails(): Promise { + if (this._emails) return this._emails; + const page = await this.http.get('/sigaa/portais/discente/discente.jsf'); + if (page.statusCode === 200) { + const buttons = page + .$('#perfil-docente .pessoal-docente') + .find('a[onclick]') + .toArray(); + + for (const button of buttons) { + const buttonName = this.parser.removeTagsHtml(page.$(button).html()); + if (buttonName === 'Meus Dados Pessoais') { + const buttonOnClick = page.$(button).attr('onclick'); + if (buttonOnClick) { + const form = page.parseJSFCLJS(buttonOnClick); + const _page = await this.http.post( + form.action.href, + form.postValues + ); + const myPersonalDataPage = await this.http.followAllRedirect(_page); + const InputElement = myPersonalDataPage + .$('table tbody') // tr td[colspan="3"]') + .find("input[name='formDiscente:txtEmail']"); + + const email = InputElement.attr('value'); + if (!email) + throw new Error('SIGAA: No e-mail on discente data page'); + + this._emails = []; + this._emails.push(email); + } + } + break; + } + if (this._emails) return this._emails; + return []; + } else { + throw new Error('SIGAA: Unexpected status code at student profile page.'); + } + } + + /** + * Change the password of account. + * @param oldPassword current password. + * @param newPassword new password. + * @throws {errorInvalidCredentials} If current password is not correct. + * @throws {errorInsufficientPasswordComplexity} If the new password does not have the complexity requirement. + */ + async changePassword( + oldPassword: string, + newPassword: string + ): Promise { + const formPage = await this.http.get('/sigaa/alterar_dados.jsf'); + if (formPage.statusCode !== 302) + throw new Error('SIGAA: Unexpected status code at change password form.'); + + const prePage = await this.http.followAllRedirect(formPage); + if ( + prePage.statusCode !== 200 || + !prePage.url.href.includes('usuario/alterar_dados.jsf') + ) + throw new Error('SIGAA: Invalid pre page at change password.'); + + const preFormElement = prePage.$('form[name="form"]'); + + const preAction = preFormElement.attr('action'); + if (!preAction) + throw new Error( + 'SIGAA: Form without action at change password pre page.' + ); + + const preActionUrl = new URL(preAction, prePage.url.href); + + const prePostValues: Record = {}; + + const preInputs = preFormElement + .find("input[name]:not([type='submit'])") + .toArray(); + for (const input of preInputs) { + const name = prePage.$(input).attr('name'); + if (name) { + prePostValues[name] = prePage.$(input).val(); + } + } + prePostValues['form:alterarSenha'] = 'form:alterarSenha'; + const page = await this.http.post(preActionUrl.href, prePostValues); + const formElement = page.$('form[name="form"]'); + + const action = formElement.attr('action'); + if (!action) + throw new Error('SIGAA: Form without action at change password page.'); + const formAction = new URL(action, page.url.href); + + const postValues: Record = {}; + const inputs = formElement + .find("input[name]:not([type='submit'])") + .toArray(); + for (const input of inputs) { + const name = page.$(input).attr('name'); + if (name) { + postValues[name] = prePage.$(input).val(); + } + } + + postValues['form:senhaAtual'] = oldPassword; + postValues['form:novaSenha'] = newPassword; + postValues['form:repetnNovaSenha'] = newPassword; + postValues['form:alterarDados'] = 'Alterar Dados'; + + const resultPage = await this.http.post(formAction.href, postValues); + + if (resultPage.statusCode === 200) { + const errorMsg = this.parser.removeTagsHtml( + resultPage.$('.erros li').html() + ); + if (errorMsg.includes('A senha digitada é muito simples.')) { + throw new Error(this.errorInsufficientPasswordComplexity); + } + if (errorMsg.includes('Senha Atual digitada não confere')) { + throw new Error(this.errorInvalidCredentials); + } + } + + if (resultPage.statusCode !== 302) { + throw new Error( + 'SIGAA: The change password page status code is different than expected.' + ); + } + } +} diff --git a/src/bonds/sigaa-student-bond.ts b/src/bonds/sigaa-student-bond.ts index 09c7228..60b712b 100755 --- a/src/bonds/sigaa-student-bond.ts +++ b/src/bonds/sigaa-student-bond.ts @@ -122,7 +122,10 @@ export class SigaaStudentBond implements StudentBond { button: null }; - const tableHeaderCellElements = table.find('thead > tr td').toArray(); + var tableHeaderCellElements = table.find('thead > tr td').toArray(); + if (!tableHeaderCellElements.length) + tableHeaderCellElements = table.find('thead > tr th').toArray(); + for (let column = 0; column < tableHeaderCellElements.length; column++) { const cellContent = this.parser.removeTagsHtml( coursesPage.$(tableHeaderCellElements[column]).html() diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index 9926c18..e89dbfd 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -14,6 +14,7 @@ import { CourseResourcesManagerFactory } from './sigaa-course-resources-manager- import { Exam } from '@courseResources/sigaa-exam-student'; import { Syllabus } from '@courseResources/sigaa-syllabus-student'; import { LessonParserFactory } from './sigaa-lesson-parser-factory'; +import { SubMenuStatus } from '../sigaa-types'; import { GradeGroup, @@ -335,7 +336,7 @@ export class SigaaCourseStudent implements CourseStudent { private async getCourseSubMenu( buttonLabel: string, retry = true - ): Promise { + ): Promise { if (buttonLabel === this.currentCoursePage) { if (this.currentPageCache) return this.currentPageCache; } @@ -366,6 +367,13 @@ export class SigaaCourseStudent implements CourseStudent { form.action.href, form.postValues ); + + if (buttonLabel === 'Ver Notas') { + if ( + pageResponse.bodyDecoded.includes('Ainda não foram lançadas notas.') + ) + return SubMenuStatus.NoExist; + } this.verifyIfCoursePageIsValid(pageResponse, buttonLabel); if (pageResponse.bodyDecoded.includes('Menu Turma Virtual')) { this.currentPageCache = pageResponse; @@ -426,6 +434,8 @@ export class SigaaCourseStudent implements CourseStudent { */ async getLessons(): Promise { const pageLessonsOne = await this.getCourseSubMenu('Principal'); + if (pageLessonsOne == SubMenuStatus.NoExist) return []; + let onclick; const pageLessonsOneType = @@ -476,6 +486,8 @@ export class SigaaCourseStudent implements CourseStudent { */ async getFiles(): Promise { const page = await this.getCourseSubMenu('Arquivos'); + if (page === SubMenuStatus.NoExist) return []; + const table = page.$('.listing'); const usedFilesId = []; if (table.length !== 0) { @@ -514,6 +526,7 @@ export class SigaaCourseStudent implements CourseStudent { */ async getForums(): Promise { const page = await this.getCourseSubMenu('Fóruns'); + if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); const usedForumIds: string[] = []; @@ -564,6 +577,8 @@ export class SigaaCourseStudent implements CourseStudent { */ async getNews(): Promise { const page = await this.getCourseSubMenu('Notícias'); + if (page === SubMenuStatus.NoExist) return []; + const table = page.$('.listing'); const usedNewsId = []; if (table.length !== 0) { @@ -600,6 +615,9 @@ export class SigaaCourseStudent implements CourseStudent { */ async getAbsence(): Promise { const page = await this.getCourseSubMenu('Frequência'); + if (page === SubMenuStatus.NoExist) + return { maxAbsences: 0, totalAbsences: 0, list: [] }; + const table = page.$('.listing'); const absences: AbsenceDay[] = []; const rows = table.find('tr[class]').toArray(); @@ -686,6 +704,7 @@ export class SigaaCourseStudent implements CourseStudent { */ async getQuizzes(): Promise { const page = await this.getCourseSubMenu('Questionários'); + if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); @@ -763,6 +782,7 @@ export class SigaaCourseStudent implements CourseStudent { */ async getWebContents(): Promise { const page = await this.getCourseSubMenu('Conteúdo/Página web'); + if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); @@ -815,6 +835,8 @@ export class SigaaCourseStudent implements CourseStudent { */ async getHomeworks(): Promise { const page = await this.getCourseSubMenu('Tarefas'); + if (page === SubMenuStatus.NoExist) return []; + const tables = page.$('.listing').toArray(); if (!tables) return []; const usedHomeworksIds = []; @@ -915,6 +937,7 @@ export class SigaaCourseStudent implements CourseStudent { */ async getMembers(): Promise { const page = await this.getCourseSubMenu('Participantes'); + if (page === SubMenuStatus.NoExist) return { teachers: [], students: [] }; const tables = page.$('table.participantes').toArray(); const tablesNames = page.$('fieldset').toArray(); @@ -1101,7 +1124,11 @@ export class SigaaCourseStudent implements CourseStudent { */ async getGrades(retry = true): Promise { try { + const grades: GradeGroup[] = []; + const page = await this.getCourseSubMenu('Ver Notas', retry); + if (page === SubMenuStatus.NoExist) return grades; + const getPositionByCellColSpan = ( ths: cheerio.Cheerio, cell: cheerio.Element @@ -1127,7 +1154,6 @@ export class SigaaCourseStudent implements CourseStudent { if (valueCells.length === 0) { throw new Error('SIGAA: Page grades without grades.'); } - const grades: GradeGroup[] = []; const theadElements: cheerio.Cheerio[] = []; for (const theadTr of theadTrs) { @@ -1284,6 +1310,14 @@ export class SigaaCourseStudent implements CourseStudent { */ async getSyllabus(): Promise { const page = await this.getCourseSubMenu('Plano de Ensino'); + if (page === SubMenuStatus.NoExist) + return { + schedule: [], + evaluations: [], + basicReferences: [], + supplementaryReferences: [] + }; + const tables = page.$('table.listagem').toArray(); const response: Syllabus = { diff --git a/src/session/login/sigaa-login-unilab.ts b/src/session/login/sigaa-login-unilab.ts new file mode 100644 index 0000000..89a2880 --- /dev/null +++ b/src/session/login/sigaa-login-unilab.ts @@ -0,0 +1,102 @@ +import { LoginStatus } from '../../sigaa-types'; +import { URL } from 'url'; +import { HTTP } from '../sigaa-http'; +import { Page, SigaaForm } from '../sigaa-page'; +import { Session } from '../sigaa-session'; +import { Login } from './sigaa-login'; + +/** + * Responsible for logging in IFSC. + * @category Internal + */ +export class SigaaLoginUNILAB implements Login { + constructor(protected http: HTTP, protected session: Session) {} + readonly errorInvalidCredentials = 'SIGAA: Invalid credentials.'; + + protected parseLoginForm(page: Page): SigaaForm { + const formElement = page.$("form[name='loginForm']"); + + const actionUrl = formElement.attr('action'); + if (!actionUrl) throw new Error('SIGAA: No action form on login page.'); + + const action = new URL(actionUrl, page.url.href); + + const postValues: Record = {}; + + formElement.find('input').each((index, element) => { + const name = page.$(element).attr('name'); + if (name) postValues[name] = page.$(element).val(); + }); + + return { action, postValues }; + } + + /** + * Current login form. + */ + protected form?: SigaaForm; + + /** + * Retuns HTML form + */ + async getLoginForm(): Promise { + if (this.form) { + return this.form; + } else { + const page = await this.http.get('/sigaa/verTelaLogin.do'); + return this.parseLoginForm(page); + } + } + + /** + * Start a session on desktop + * @param username + * @param password + */ + protected async desktopLogin( + username: string, + password: string + ): Promise { + const { action, postValues } = await this.getLoginForm(); + + postValues['user.login'] = username; + postValues['user.senha'] = password; + const page = await this.http.post(action.href, postValues); + return await this.parseDesktopLoginResult(page); + } + + /** + * Start a session on Sigaa, return login reponse page + * @param username + * @param password + */ + async login(username: string, password: string, retry = true): Promise { + if (this.session.loginStatus === LoginStatus.Authenticated) + throw new Error('SIGAA: This session already has a user logged in.'); + try { + const page = await this.desktopLogin(username, password); + return this.http.followAllRedirect(page); + } catch (error) { + if (!retry || error.message === this.errorInvalidCredentials) { + throw error; + } else { + return this.login(username, password, false); + } + } + } + + protected async parseDesktopLoginResult(page: Page): Promise { + const accountPage = await this.http.followAllRedirect(page); + if (accountPage.bodyDecoded.includes('Entrar no Sistema')) { + if (accountPage.bodyDecoded.includes('Usuário e/ou senha inválidos')) { + this.form = await this.parseLoginForm(accountPage); + throw new Error(this.errorInvalidCredentials); + } else { + throw new Error('SIGAA: Invalid response after login attempt.'); + } + } else { + this.session.loginStatus = LoginStatus.Authenticated; + return accountPage; + } + } +} diff --git a/src/session/sigaa-session.ts b/src/session/sigaa-session.ts index a747564..bf15358 100644 --- a/src/session/sigaa-session.ts +++ b/src/session/sigaa-session.ts @@ -4,7 +4,7 @@ import { LoginStatus } from '../sigaa-types'; * The institution serves to adjust interactions with SIGAA. * @category Public */ -export type InstitutionType = 'IFSC' | 'UFPB'; +export type InstitutionType = 'IFSC' | 'UFPB' | 'UNILAB'; /** * Sigaa session control diff --git a/src/sigaa-main.ts b/src/sigaa-main.ts index 1476e95..b09c2ca 100755 --- a/src/sigaa-main.ts +++ b/src/sigaa-main.ts @@ -9,6 +9,7 @@ import { HTTPFactory, SigaaHTTPFactory } from '@session/sigaa-http-factory'; import { Login } from '@session/login/sigaa-login'; import { SigaaLoginIFSC } from '@session/login/sigaa-login-ifsc'; import { SigaaLoginUFPB } from '@session/login/sigaa-login-ufpb'; +import { SigaaLoginUNILAB } from '@session/login/sigaa-login-unilab'; import { InstitutionType, Session, SigaaSession } from '@session/sigaa-session'; import { SigaaCookiesController } from '@session/sigaa-cookies-controller'; import { SigaaPageCacheWithBond } from '@session/sigaa-page-cache-with-bond'; @@ -294,10 +295,27 @@ export class Sigaa { ); } + switch (options.login || options.institution) { + case 'IFSC': + this.loginInstance = new SigaaLoginIFSC(this.http, this.session); + break; + case 'UFPB': + this.loginInstance = new SigaaLoginUFPB(this.http, this.session); + break; + case 'UNILAB': + this.loginInstance = new SigaaLoginUNILAB(this.http, this.session); + break; + default: + this.loginInstance = new SigaaLoginIFSC(this.http, this.session); + break; + } + + /* this.loginInstance = options.login || options.institution === 'UFPB' ? new SigaaLoginUFPB(this.http, this.session) : new SigaaLoginIFSC(this.http, this.session); + */ } /** diff --git a/src/sigaa-types.ts b/src/sigaa-types.ts index df9e155..01396a1 100644 --- a/src/sigaa-types.ts +++ b/src/sigaa-types.ts @@ -7,6 +7,14 @@ export enum LoginStatus { Authenticated } +/** + * Grade SubMenus Status + * @category Public + */ +export enum SubMenuStatus { + NoExist +} + /** * @category Internal */ From 1d4468bf6dcb6ab7567e6f31051bfb5d7fe6eea7 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Thu, 13 Apr 2023 21:01:13 -0300 Subject: [PATCH 02/11] Adicionado suporte a faltas - UNILAB --- .../resources/sigaa-absence-list-student.ts | 1 + src/courses/sigaa-course-student.ts | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/courses/resources/sigaa-absence-list-student.ts b/src/courses/resources/sigaa-absence-list-student.ts index ba887c0..53616d4 100644 --- a/src/courses/resources/sigaa-absence-list-student.ts +++ b/src/courses/resources/sigaa-absence-list-student.ts @@ -13,4 +13,5 @@ export interface AbsenceList { list: AbsenceDay[]; totalAbsences: number; maxAbsences: number; + totalWorkload?: number; } diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index e89dbfd..94613ec 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -638,27 +638,65 @@ export class SigaaCourseStudent implements CourseStudent { numOfAbsences }); } + const details = this.parser .removeTagsHtml(page.$('.botoes-show').html()) .split('\n'); - - let totalAbsences, maxAbsences; + const descriptions = this.parser + .removeTagsHtml(page.$('.descricaoOperacao').html()) + .split('\n'); + let totalAbsences, + maxAbsences, + minPercentageOfAttendance, + totalAttendance, + totalGivenClasses, + totalWorkload; for (const detail of details) { if (detail.includes('Total de Faltas')) { totalAbsences = parseInt(detail.replace(/\D/gm, ''), 10); + } else if (detail.includes('Frequência:')) { + totalAttendance = parseInt(detail.replace(/\D/gm, ''), 10); + } else if (detail.includes('Número de Aulas Ministradas')) { + totalGivenClasses = parseInt(detail.replace(/\D/gm, ''), 10); } else if (detail.includes('Máximo de Faltas Permitido')) { maxAbsences = parseInt(detail.replace(/\D/gm, ''), 10); + } else if ( + detail.includes('Número de Aulas definidas pela CH do Componente') + ) { + totalWorkload = parseInt(detail.replace(/\D/gm, ''), 10); + } + } + + if (descriptions) { + for (const description of descriptions) { + if (description.includes('Tiver presença em um número de aulas')) { + minPercentageOfAttendance = parseInt( + description.replace(/\D/gm, '').slice(0, -1), + 10 + ); + } } } + if ( + typeof totalWorkload === 'number' && + typeof minPercentageOfAttendance === 'number' + ) + maxAbsences = totalWorkload * ((100 - minPercentageOfAttendance) / 100); + if ( + typeof totalAttendance === 'number' && + typeof totalGivenClasses === 'number' + ) + totalAbsences = totalGivenClasses - totalAttendance; if (typeof maxAbsences !== 'number' || typeof totalAbsences !== 'number') throw new Error('SIGAA: Invalid absence page format.'); return { list: absences, totalAbsences, - maxAbsences + maxAbsences, + totalWorkload }; } From 5ee60f689e9c383d1872098011b776ef7c362137 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Fri, 21 Apr 2023 16:01:02 -0300 Subject: [PATCH 03/11] Adicionado suporte aos topicos de aula (lessons) - UNILAB --- src/courses/sigaa-lesson-parser.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/courses/sigaa-lesson-parser.ts b/src/courses/sigaa-lesson-parser.ts index ae8eced..4f26189 100644 --- a/src/courses/sigaa-lesson-parser.ts +++ b/src/courses/sigaa-lesson-parser.ts @@ -210,7 +210,7 @@ export class SigaaLessonParser implements LessonParser { .toArray(); if (attachmentElements.length !== 0) { for (const attachmentElement of attachmentElements) { - const iconElement = page.$(attachmentElement).find('img'); + const iconElement = page.$(attachmentElement).children('img'); const iconSrc = iconElement.attr('src'); try { if (iconSrc === undefined) { @@ -305,13 +305,18 @@ export class SigaaLessonParser implements LessonParser { page: Page, attachmentElement: cheerio.Element ): GenericAttachmentData { - const titleElement = page + var titleElement = page .$(attachmentElement) .find('span') .children() .first(); - const title = this.parser.removeTagsHtml(titleElement.html()); - const titleOnClick = titleElement.attr('onclick'); + var title = this.parser.removeTagsHtml(titleElement.html()); + var titleOnClick = titleElement.attr('onclick'); + if (!titleOnClick) { + titleElement = page.$(attachmentElement).find('a'); + title = this.parser.removeTagsHtml(titleElement.html()); + titleOnClick = titleElement.attr('onclick'); + } if (!titleOnClick) throw new Error('SIGAA: Attachment title without onclick event.'); const form = page.parseJSFCLJS(titleOnClick); From a4e280c7f0460d13a9c6bb144145af4a702677b8 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Wed, 23 Aug 2023 09:03:55 -0300 Subject: [PATCH 04/11] =?UTF-8?q?Pulando=20pagina=20de=20avalia=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20curso=20-=20UNILAB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/account/sigaa-account-unilab.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/account/sigaa-account-unilab.ts b/src/account/sigaa-account-unilab.ts index 457eb35..e8f1523 100755 --- a/src/account/sigaa-account-unilab.ts +++ b/src/account/sigaa-account-unilab.ts @@ -94,6 +94,11 @@ export class SigaaAccountUNILAB implements Account { this.pagehomeParsePromise = this.http .get(homepage.url.href, { noCache: true }) .then((page) => this.parseBondPage(page)); + } else if (homepage.url.href.includes('/sigaa/avaliacao/introDiscente.jsf')) { + //If it is in course avaliation page. (Bypassing the course avaliation page) + this.pagehomeParsePromise = this.http.get(homepage.url.origin + "/sigaa/portais/discente/discente.jsf", { + noCache: true + }).then(page => this.parseStudentHomePage(page)); } else { throw new Error('SIGAA: Unknown homepage format.'); } From 38c2bff4097ba1b4e1123d4e84925c5e1b469dc3 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Sun, 3 Sep 2023 09:38:36 -0300 Subject: [PATCH 05/11] =?UTF-8?q?Removido=20tipagens=20e=20verifica=C3=A7?= =?UTF-8?q?=C3=B5es=20alteradas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bonds/sigaa-student-bond.ts | 2 +- src/courses/sigaa-course-student.ts | 52 ++++++++++------------------- src/courses/sigaa-lesson-parser.ts | 2 +- src/sigaa-types.ts | 8 ----- 4 files changed, 20 insertions(+), 44 deletions(-) diff --git a/src/bonds/sigaa-student-bond.ts b/src/bonds/sigaa-student-bond.ts index 60b712b..145682b 100755 --- a/src/bonds/sigaa-student-bond.ts +++ b/src/bonds/sigaa-student-bond.ts @@ -122,7 +122,7 @@ export class SigaaStudentBond implements StudentBond { button: null }; - var tableHeaderCellElements = table.find('thead > tr td').toArray(); + let tableHeaderCellElements = table.find('thead > tr td').toArray(); if (!tableHeaderCellElements.length) tableHeaderCellElements = table.find('thead > tr th').toArray(); diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index 94613ec..7613ab4 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -14,7 +14,6 @@ import { CourseResourcesManagerFactory } from './sigaa-course-resources-manager- import { Exam } from '@courseResources/sigaa-exam-student'; import { Syllabus } from '@courseResources/sigaa-syllabus-student'; import { LessonParserFactory } from './sigaa-lesson-parser-factory'; -import { SubMenuStatus } from '../sigaa-types'; import { GradeGroup, @@ -311,14 +310,24 @@ export class SigaaCourseStudent implements CourseStudent { throw new Error('SIGAA: Invalid course page status code.'); let pageCourseCode: string | undefined; + + const relatorioH3 = page.$('#relatorio h3').html(); + const linkCodigoTurma = page.$('#linkCodigoTurma').html(); + if (buttonLabel === 'Ver Notas') { - pageCourseCode = this.parser - .removeTagsHtml(page.$('#relatorio h3').html()) - .split(' - ')[0]; + if (relatorioH3) { + pageCourseCode = this.parser.removeTagsHtml(relatorioH3).split(' - ')[0]; + } else if (linkCodigoTurma){ + pageCourseCode = this.parser.removeTagsHtml(linkCodigoTurma).replace(/ -$/, ''); + } } else { - pageCourseCode = this.parser - .removeTagsHtml(page.$('#linkCodigoTurma').html()) - .replace(/ -$/, ''); + if (linkCodigoTurma) { + pageCourseCode = this.parser.removeTagsHtml(linkCodigoTurma).replace(/ -$/, ''); + } + } + + if (!pageCourseCode) { + throw new Error('SIGAA: Course code not found on the page.'); } if (pageCourseCode !== this.code) { @@ -336,7 +345,7 @@ export class SigaaCourseStudent implements CourseStudent { private async getCourseSubMenu( buttonLabel: string, retry = true - ): Promise { + ): Promise { if (buttonLabel === this.currentCoursePage) { if (this.currentPageCache) return this.currentPageCache; } @@ -368,12 +377,6 @@ export class SigaaCourseStudent implements CourseStudent { form.postValues ); - if (buttonLabel === 'Ver Notas') { - if ( - pageResponse.bodyDecoded.includes('Ainda não foram lançadas notas.') - ) - return SubMenuStatus.NoExist; - } this.verifyIfCoursePageIsValid(pageResponse, buttonLabel); if (pageResponse.bodyDecoded.includes('Menu Turma Virtual')) { this.currentPageCache = pageResponse; @@ -434,7 +437,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getLessons(): Promise { const pageLessonsOne = await this.getCourseSubMenu('Principal'); - if (pageLessonsOne == SubMenuStatus.NoExist) return []; let onclick; @@ -486,7 +488,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getFiles(): Promise { const page = await this.getCourseSubMenu('Arquivos'); - if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); const usedFilesId = []; @@ -526,7 +527,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getForums(): Promise { const page = await this.getCourseSubMenu('Fóruns'); - if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); const usedForumIds: string[] = []; @@ -577,7 +577,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getNews(): Promise { const page = await this.getCourseSubMenu('Notícias'); - if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); const usedNewsId = []; @@ -615,9 +614,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getAbsence(): Promise { const page = await this.getCourseSubMenu('Frequência'); - if (page === SubMenuStatus.NoExist) - return { maxAbsences: 0, totalAbsences: 0, list: [] }; - const table = page.$('.listing'); const absences: AbsenceDay[] = []; const rows = table.find('tr[class]').toArray(); @@ -742,8 +738,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getQuizzes(): Promise { const page = await this.getCourseSubMenu('Questionários'); - if (page === SubMenuStatus.NoExist) return []; - const table = page.$('.listing'); const usedQuizzesIds = []; @@ -820,7 +814,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getWebContents(): Promise { const page = await this.getCourseSubMenu('Conteúdo/Página web'); - if (page === SubMenuStatus.NoExist) return []; const table = page.$('.listing'); @@ -873,7 +866,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getHomeworks(): Promise { const page = await this.getCourseSubMenu('Tarefas'); - if (page === SubMenuStatus.NoExist) return []; const tables = page.$('.listing').toArray(); if (!tables) return []; @@ -975,7 +967,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getMembers(): Promise { const page = await this.getCourseSubMenu('Participantes'); - if (page === SubMenuStatus.NoExist) return { teachers: [], students: [] }; const tables = page.$('table.participantes').toArray(); const tablesNames = page.$('fieldset').toArray(); @@ -1165,7 +1156,7 @@ export class SigaaCourseStudent implements CourseStudent { const grades: GradeGroup[] = []; const page = await this.getCourseSubMenu('Ver Notas', retry); - if (page === SubMenuStatus.NoExist) return grades; + if (page.bodyDecoded.includes('Ainda não foram lançadas notas.')) return grades; const getPositionByCellColSpan = ( ths: cheerio.Cheerio, @@ -1348,13 +1339,6 @@ export class SigaaCourseStudent implements CourseStudent { */ async getSyllabus(): Promise { const page = await this.getCourseSubMenu('Plano de Ensino'); - if (page === SubMenuStatus.NoExist) - return { - schedule: [], - evaluations: [], - basicReferences: [], - supplementaryReferences: [] - }; const tables = page.$('table.listagem').toArray(); diff --git a/src/courses/sigaa-lesson-parser.ts b/src/courses/sigaa-lesson-parser.ts index 4f26189..904000d 100644 --- a/src/courses/sigaa-lesson-parser.ts +++ b/src/courses/sigaa-lesson-parser.ts @@ -305,7 +305,7 @@ export class SigaaLessonParser implements LessonParser { page: Page, attachmentElement: cheerio.Element ): GenericAttachmentData { - var titleElement = page + let titleElement = page .$(attachmentElement) .find('span') .children() diff --git a/src/sigaa-types.ts b/src/sigaa-types.ts index 01396a1..df9e155 100644 --- a/src/sigaa-types.ts +++ b/src/sigaa-types.ts @@ -7,14 +7,6 @@ export enum LoginStatus { Authenticated } -/** - * Grade SubMenus Status - * @category Public - */ -export enum SubMenuStatus { - NoExist -} - /** * @category Internal */ From f270bd41d4f7d442d145084e4526a707c6ce3b39 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Mon, 11 Sep 2023 16:38:55 -0300 Subject: [PATCH 06/11] =?UTF-8?q?Leves=20atualiza=C3=A7=C3=B5es=20-=20UNIL?= =?UTF-8?q?AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/account/sigaa-account.ts | 4 +- src/session/login/sigaa-login-unilab.ts | 12 ++-- src/session/page/sigaa-page-unilab.ts | 77 +++++++++++++++++++++ src/session/sigaa-http.ts | 4 +- src/session/sigaa-institution-controller.ts | 14 ++-- src/session/sigaa-page.ts | 5 +- src/sigaa-all-types.ts | 2 + 7 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/session/page/sigaa-page-unilab.ts diff --git a/src/account/sigaa-account.ts b/src/account/sigaa-account.ts index ccfb761..d697c93 100755 --- a/src/account/sigaa-account.ts +++ b/src/account/sigaa-account.ts @@ -4,6 +4,7 @@ import { BondType } from '@bonds/sigaa-bond-factory'; import { SigaaAccountIFSC } from './sigaa-account-ifsc'; import { SigaaAccountUFPB } from './sigaa-account-ufpb'; import { SigaaAccountUNB } from './sigaa-account-unb'; +import { SigaaAccountUNILAB } from './sigaa-account-unilab'; /** * Abstraction of account type. @@ -73,6 +74,7 @@ export interface Account { export type SigaaAccountInstitution = | SigaaAccountIFSC | SigaaAccountUFPB - | SigaaAccountUNB; + | SigaaAccountUNB + | SigaaAccountUNILAB; export type CommonSigaaAccount = Account & SigaaAccountInstitution; diff --git a/src/session/login/sigaa-login-unilab.ts b/src/session/login/sigaa-login-unilab.ts index 89a2880..79fdbcd 100644 --- a/src/session/login/sigaa-login-unilab.ts +++ b/src/session/login/sigaa-login-unilab.ts @@ -1,9 +1,11 @@ import { LoginStatus } from '../../sigaa-types'; import { URL } from 'url'; import { HTTP } from '../sigaa-http'; -import { Page, SigaaForm } from '../sigaa-page'; +//import { Page, SigaaForm } from '../sigaa-page'; import { Session } from '../sigaa-session'; import { Login } from './sigaa-login'; +import { UNILABPage } from '@session/page/sigaa-page-unilab'; +import { SigaaForm } from '@session/sigaa-page'; /** * Responsible for logging in IFSC. @@ -13,7 +15,7 @@ export class SigaaLoginUNILAB implements Login { constructor(protected http: HTTP, protected session: Session) {} readonly errorInvalidCredentials = 'SIGAA: Invalid credentials.'; - protected parseLoginForm(page: Page): SigaaForm { + protected parseLoginForm(page: UNILABPage): SigaaForm { const formElement = page.$("form[name='loginForm']"); const actionUrl = formElement.attr('action'); @@ -56,7 +58,7 @@ export class SigaaLoginUNILAB implements Login { protected async desktopLogin( username: string, password: string - ): Promise { + ): Promise { const { action, postValues } = await this.getLoginForm(); postValues['user.login'] = username; @@ -70,7 +72,7 @@ export class SigaaLoginUNILAB implements Login { * @param username * @param password */ - async login(username: string, password: string, retry = true): Promise { + async login(username: string, password: string, retry = true): Promise { if (this.session.loginStatus === LoginStatus.Authenticated) throw new Error('SIGAA: This session already has a user logged in.'); try { @@ -85,7 +87,7 @@ export class SigaaLoginUNILAB implements Login { } } - protected async parseDesktopLoginResult(page: Page): Promise { + protected async parseDesktopLoginResult(page: UNILABPage): Promise { const accountPage = await this.http.followAllRedirect(page); if (accountPage.bodyDecoded.includes('Entrar no Sistema')) { if (accountPage.bodyDecoded.includes('Usuário e/ou senha inválidos')) { diff --git a/src/session/page/sigaa-page-unilab.ts b/src/session/page/sigaa-page-unilab.ts new file mode 100644 index 0000000..0b6eafe --- /dev/null +++ b/src/session/page/sigaa-page-unilab.ts @@ -0,0 +1,77 @@ +import { + CommonPage, + SigaaForm, + CommonSigaaPage, + SigaaPageConstructor + } from '@session/sigaa-page'; + import { URL } from 'url'; + + /** + * @category Internal + */ + export interface UNILABPage extends CommonPage { + /** + * Extracts the javascript function JSFCLJS from the page, + * this function on the page redirects the user to another + * page using the POST method, often this function is in + * the onclick attribute in some element. + * @param javaScriptCode + * @returns Object with URL action and POST values equivalent to function + */ + parseJSFCLJS(javaScriptCode: string): SigaaForm; + } + + /** + * Response page of sigaa. + * @category Internal + */ + export class SigaaPageUNILAB extends CommonSigaaPage { + constructor(options: SigaaPageConstructor) { + super(options); + } + + /** + * @inheritdoc + */ + parseJSFCLJS(javaScriptCode: string): SigaaForm { + if (!javaScriptCode.includes('getElementById')) + throw new Error('SIGAA: Form not found.'); + + const formQuery = javaScriptCode.match( + /document\.getElementById\('(\w+)'\)/ + ); + if (!formQuery) throw new Error('SIGAA: Form without id.'); + + const formEl = this.$(`#${formQuery[1]}`); + if (!formEl) { + throw new Error('SIGAA: Form not found.'); + } + + const formAction = formEl.attr('action'); + if (formAction === undefined) + throw new Error('SIGAA: Form without action.'); + + const action = new URL(formAction, this.url); + const postValues: Record = {}; + + formEl.find("input:not([type='submit'])").each((_, element) => { + const name = this.$(element).attr('name'); + const value = this.$(element).val(); + if (name !== undefined) { + postValues[name] = value; + } + }); + const formPostValuesString = `{${javaScriptCode + .replace(/if([\S\s]*?),{|},([\S\s]*?)false/gm, '') + .replace(/"/gm, '\\"') + .replace(/'/gm, '"')}}`; + return { + action, + postValues: { + ...postValues, + ...JSON.parse(formPostValuesString) + } + }; + } + } + \ No newline at end of file diff --git a/src/session/sigaa-http.ts b/src/session/sigaa-http.ts index 5b00294..a28501e 100755 --- a/src/session/sigaa-http.ts +++ b/src/session/sigaa-http.ts @@ -15,6 +15,7 @@ import { SigaaPageInstitutionMap } from './sigaa-institution-controller'; import { SigaaPageIFSC } from './page/sigaa-page-ifsc'; import { SigaaPageUFPB } from './page/sigaa-page-ufpb'; import { SigaaPageUNB } from './page/sigaa-page-unb'; +import { SigaaPageUNILAB } from './page/sigaa-page-unilab'; /** * @category Public @@ -465,7 +466,8 @@ export class SigaaHTTP implements HTTP { const SigaaPageInstitution: SigaaPageInstitutionMap = { IFSC: SigaaPageIFSC, UFPB: SigaaPageUFPB, - UNB: SigaaPageUNB + UNB: SigaaPageUNB, + UNILAB: SigaaPageUNILAB }; const page = new SigaaPageInstitution[ this.httpSession.institutionController.institution diff --git a/src/session/sigaa-institution-controller.ts b/src/session/sigaa-institution-controller.ts index 1c62645..4a95d87 100644 --- a/src/session/sigaa-institution-controller.ts +++ b/src/session/sigaa-institution-controller.ts @@ -1,16 +1,19 @@ import { SigaaAccountIFSC } from '@account/sigaa-account-ifsc'; import { SigaaAccountUFPB } from '@account/sigaa-account-ufpb'; import { SigaaAccountUNB } from '@account/sigaa-account-unb'; +import { SigaaAccountUNILAB } from '@account/sigaa-account-unilab'; import { SigaaLoginIFSC } from './login/sigaa-login-ifsc'; import { SigaaLoginUFPB } from './login/sigaa-login-ufpb'; import { SigaaLoginUNB } from './login/sigaa-login-unb'; +import { SigaaLoginUNILAB } from './login/sigaa-login-unilab'; import { SigaaPageIFSC } from './page/sigaa-page-ifsc'; import { SigaaPageUFPB } from './page/sigaa-page-ufpb'; import { SigaaPageUNB } from './page/sigaa-page-unb'; +import { SigaaPageUNILAB } from './page/sigaa-page-unilab'; /** * Map */ -export type InstitutionType = 'IFSC' | 'UFPB' | 'UNB'; +export type InstitutionType = 'IFSC' | 'UFPB' | 'UNB' | 'UNILAB'; export type InstitutionMap = Record; /** * Map of classes that returns SigaaLogin instance; @@ -18,7 +21,8 @@ export type InstitutionMap = Record; type SigaaLoginMap = | typeof SigaaLoginIFSC | typeof SigaaLoginUFPB - | typeof SigaaLoginUNB; + | typeof SigaaLoginUNB + | typeof SigaaLoginUNILAB; export type SigaaLoginInstitutionMap = InstitutionMap; /** @@ -27,7 +31,8 @@ export type SigaaLoginInstitutionMap = InstitutionMap; type SigaaAccountMap = | typeof SigaaAccountIFSC | typeof SigaaAccountUFPB - | typeof SigaaAccountUNB; + | typeof SigaaAccountUNB + | typeof SigaaAccountUNILAB; export type SigaaAccountInstitutionMap = InstitutionMap; /** @@ -36,7 +41,8 @@ export type SigaaAccountInstitutionMap = InstitutionMap; type SigaaPageMap = | typeof SigaaPageIFSC | typeof SigaaPageUFPB - | typeof SigaaPageUNB; + | typeof SigaaPageUNB + | typeof SigaaPageUNILAB; export type SigaaPageInstitutionMap = InstitutionMap; export interface InstitutionController { diff --git a/src/session/sigaa-page.ts b/src/session/sigaa-page.ts index 865a184..fe72245 100755 --- a/src/session/sigaa-page.ts +++ b/src/session/sigaa-page.ts @@ -7,6 +7,7 @@ import { HTTPRequestOptions } from './sigaa-http'; import { IFSCPage, SigaaPageIFSC } from './page/sigaa-page-ifsc'; import { SigaaPageUFPB, UFPBPage } from './page/sigaa-page-ufpb'; import { SigaaPageUNB, UNBPage } from './page/sigaa-page-unb'; +import { SigaaPageUNILAB, UNILABPage } from './page/sigaa-page-unilab'; /** * @category Internal @@ -105,9 +106,9 @@ export interface CommonPage { readonly requestBody?: string | Buffer; } -export type Page = CommonPage & (IFSCPage | UFPBPage | UNBPage); +export type Page = CommonPage & (IFSCPage | UFPBPage | UNBPage | UNILABPage); export type SigaaPage = CommonSigaaPage & - (SigaaPageIFSC | SigaaPageUFPB | SigaaPageUNB); + (SigaaPageIFSC | SigaaPageUFPB | SigaaPageUNB | SigaaPageUNILAB); /** * Response page of sigaa. * @category Internal diff --git a/src/sigaa-all-types.ts b/src/sigaa-all-types.ts index a0a3204..b9e5c43 100755 --- a/src/sigaa-all-types.ts +++ b/src/sigaa-all-types.ts @@ -53,6 +53,7 @@ export * from '@search/sigaa-search'; export * from '@session/login/sigaa-login-ifsc'; export * from '@session/login/sigaa-login-ufpb'; export * from '@session/login/sigaa-login-unb'; +export * from '@session/login/sigaa-login-unilab'; export * from '@session/login/sigaa-login'; export * from '@session/sigaa-bond-controller'; @@ -68,6 +69,7 @@ export * from '@session/sigaa-page'; export * from '@session/page/sigaa-page-ifsc'; export * from '@session/page/sigaa-page-ufpb'; export * from '@session/page/sigaa-page-unb'; +export * from '@session/page/sigaa-page-unilab'; export * from '@session/sigaa-institution-controller'; From 428be67efba6563163313a15c99075cc57cff1fe Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Mon, 18 Dec 2023 11:03:05 -0300 Subject: [PATCH 07/11] =?UTF-8?q?Pulando=20nova=20pagina=20de=20avalia?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20curso=20-=20UNILAB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/account/sigaa-account-unilab.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/account/sigaa-account-unilab.ts b/src/account/sigaa-account-unilab.ts index e8f1523..4aab568 100755 --- a/src/account/sigaa-account-unilab.ts +++ b/src/account/sigaa-account-unilab.ts @@ -94,11 +94,22 @@ export class SigaaAccountUNILAB implements Account { this.pagehomeParsePromise = this.http .get(homepage.url.href, { noCache: true }) .then((page) => this.parseBondPage(page)); - } else if (homepage.url.href.includes('/sigaa/avaliacao/introDiscente.jsf')) { + } else if ( + homepage.url.href.includes('/sigaa/avaliacao/introDiscente.jsf') || + homepage.url.href.includes('/sigaa/telaAvisoCpa.jsf') || + homepage.url.href.includes('/sigaa/telaAvisoLogon.jsf') + ) { //If it is in course avaliation page. (Bypassing the course avaliation page) - this.pagehomeParsePromise = this.http.get(homepage.url.origin + "/sigaa/portais/discente/discente.jsf", { - noCache: true - }).then(page => this.parseStudentHomePage(page)); + this.pagehomeParsePromise = this.http + .get(homepage.url.origin + '/sigaa/verPortalDiscente.do', { + noCache: true + }) + .then((page) => { + return this.http.followAllRedirect(page); + }) + .then((page) => { + this.parseHomepage(page); + }); } else { throw new Error('SIGAA: Unknown homepage format.'); } From ba8af72d42b0c5b62ced1c095aa3bb495b38e2b3 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Thu, 21 Dec 2023 20:01:16 -0300 Subject: [PATCH 08/11] Suporte completo a membros da turma - UNILAB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data de matricula não é apresentado em turma da UNILAB --- src/courses/resources/sigaa-member-list-student.ts | 2 +- src/courses/sigaa-course-student.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/courses/resources/sigaa-member-list-student.ts b/src/courses/resources/sigaa-member-list-student.ts index 5310c0d..f161549 100644 --- a/src/courses/resources/sigaa-member-list-student.ts +++ b/src/courses/resources/sigaa-member-list-student.ts @@ -22,7 +22,7 @@ export interface Teacher extends Member { export interface Student extends Member { registration: string; program: string; - registrationDate: Date; + registrationDate?: Date; } /** diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index 959d69e..a7a5ab7 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -1101,7 +1101,6 @@ export class SigaaCourseStudent implements CourseStudent { if ( !username || !email || - !registrationDate || !registration || !program ) From c6b7679a61281dd8d6272875314c947f3b1d91be Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Wed, 17 Jan 2024 09:57:36 -0300 Subject: [PATCH 09/11] Fix #46 UNILAB usando metodo Promise.all --- src/courses/sigaa-course-student.ts | 35 ++++++++++++----------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index a7a5ab7..c3509bb 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -308,24 +308,21 @@ export class SigaaCourseStudent implements CourseStudent { throw new Error('SIGAA: Invalid course page status code.'); let pageCourseCode: string | undefined; - - const relatorioH3 = page.$('#relatorio h3').html(); - const linkCodigoTurma = page.$('#linkCodigoTurma').html(); if (buttonLabel === 'Ver Notas') { - if (relatorioH3) { - pageCourseCode = this.parser.removeTagsHtml(relatorioH3).split(' - ')[0]; - } else if (linkCodigoTurma){ - pageCourseCode = this.parser.removeTagsHtml(linkCodigoTurma).replace(/ -$/, ''); + if (page.bodyDecoded.includes('Ainda não foram')) { + pageCourseCode = this.parser + .removeTagsHtml(page.$('#linkCodigoTurma').html()) + .replace(/ -$/, ''); + } else { + pageCourseCode = this.parser + .removeTagsHtml(page.$('#relatorio h3').html()) + .split(' - ')[0]; } } else { - if (linkCodigoTurma) { - pageCourseCode = this.parser.removeTagsHtml(linkCodigoTurma).replace(/ -$/, ''); - } - } - - if (!pageCourseCode) { - throw new Error('SIGAA: Course code not found on the page.'); + pageCourseCode = this.parser + .removeTagsHtml(page.$('#linkCodigoTurma').html()) + .replace(/ -$/, ''); } if (pageCourseCode !== this.code) { @@ -1098,12 +1095,7 @@ export class SigaaCourseStudent implements CourseStudent { } } - if ( - !username || - !email || - !registration || - !program - ) + if (!username || !email || !registration || !program) throw new Error('SIGAA: Invalid student format at member page.'); const name = this.parser.removeTagsHtml( @@ -1153,7 +1145,8 @@ export class SigaaCourseStudent implements CourseStudent { const grades: GradeGroup[] = []; const page = await this.getCourseSubMenu('Ver Notas', retry); - if (page.bodyDecoded.includes('Ainda não foram lançadas notas.')) return grades; + if (page.bodyDecoded.includes('Ainda não foram lançadas notas.')) + return grades; const getPositionByCellColSpan = ( ths: cheerio.Cheerio, From 11019a139de4c9f1149af9a0d2e2461dea433636 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Wed, 17 Jan 2024 12:48:44 -0300 Subject: [PATCH 10/11] =?UTF-8?q?Resolvido=20bug=20parse=20de=20nexo=20em?= =?UTF-8?q?=20videos=20n=C3=A3o=20embed=20UNILAB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/courses/sigaa-lesson-parser.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/courses/sigaa-lesson-parser.ts b/src/courses/sigaa-lesson-parser.ts index 904000d..ece51bc 100644 --- a/src/courses/sigaa-lesson-parser.ts +++ b/src/courses/sigaa-lesson-parser.ts @@ -416,11 +416,15 @@ export class SigaaLessonParser implements LessonParser { title = title.replace(/\(Link Externo\)$/g, ''); src = href; } else { - const titleElement = page - .$(attachmentElement) - .find('span[id] > span[id]'); + let titleElement = page.$(attachmentElement).find('span[id] > span[id]'); + if (!titleElement) { + titleElement = page.$(attachmentElement).find('span[id] > span[id] h1'); + } title = this.parser.removeTagsHtml(titleElement.html()); - const srcIframe = page.$(attachmentElement).find('iframe').attr('src'); + let srcIframe = page.$(attachmentElement).find('iframe').attr('src'); + if (!srcIframe) { + srcIframe = page.$(attachmentElement).find('embed').attr('src'); + } if (!srcIframe) throw new Error('SIGAA: Video iframe without url.'); src = srcIframe; } From 53296f5a280a811250481e09906e6385d22fc853 Mon Sep 17 00:00:00 2001 From: deAssis Filho Date: Sun, 21 Jan 2024 08:07:21 -0300 Subject: [PATCH 11/11] Adicionado suporte a local de aula do semestre atual --- src/bonds/sigaa-student-bond.ts | 87 +++++++++++++++++++++++++++++ src/courses/sigaa-course-student.ts | 16 +++++- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/bonds/sigaa-student-bond.ts b/src/bonds/sigaa-student-bond.ts index fc7997d..2a77a11 100755 --- a/src/bonds/sigaa-student-bond.ts +++ b/src/bonds/sigaa-student-bond.ts @@ -29,6 +29,8 @@ export interface StudentBond { * @param allPeriods if true, all courses will be returned; otherwise, only current courses. * @returns Promise with array of courses. */ + getCoursesLocal(): Promise[]>; + getCourses(allPeriods?: boolean): Promise; getActivities(): Promise; @@ -69,6 +71,84 @@ export class SigaaStudentBond implements StudentBond { readonly type = 'student'; private _currentPeriod?: string; + + async getCoursesLocal(): Promise[]> { + const frontPage = await this.http.get( + '/sigaa/portais/discente/discente.jsf' + ); + + const coursesLocal = []; + + const table = frontPage.$('#turmas-portal table:nth-child(3)'); + if (table.length === 0) return []; + + const rows = table.find('tbody > tr').toArray(); + + const tableColumnIndexs: Record = { + title: null, + classLocal: null, + schedule: null + }; + + let tableHeaderCellElements = table.find('thead > tr td').toArray(); + if (!tableHeaderCellElements.length) + tableHeaderCellElements = table.find('thead > tr th').toArray(); + + for (let column = 0; column < tableHeaderCellElements.length; column++) { + const cellContent = this.parser.removeTagsHtml( + frontPage.$(tableHeaderCellElements[column]).html() + ); + switch (cellContent) { + case 'Componente Curricular': + tableColumnIndexs.title = column; + break; + case 'Local': + tableColumnIndexs.classLocal = column; + break; + case 'Horário': + tableColumnIndexs.schedule = column; + break; + } + } + + if (tableColumnIndexs.title == null) { + throw new Error( + 'SIGAA: Invalid courses table, could not find the column with class titles.' + ); + } + if (tableColumnIndexs.classLocal == null) { + throw new Error( + 'SIGAA: Invalid courses table, could not find the column with class local.' + ); + } + if (tableColumnIndexs.schedule == null) { + throw new Error( + 'SIGAA: Invalid courses table, could not find the column with class schedules.' + ); + } + + for (const row of rows) { + const cellElements = frontPage.$(row).find('td'); + const title = this.parser.removeTagsHtml( + cellElements.eq(tableColumnIndexs.title).html() + ); + const classLocal = this.parser.removeTagsHtml( + cellElements.eq(tableColumnIndexs.classLocal).html() + ); + const schedule = this.parser.removeTagsHtml( + cellElements.eq(tableColumnIndexs.schedule).html() + ); + + const courseData = { + title, + classLocal, + schedule + }; + coursesLocal.push(courseData); + } + return coursesLocal; + } + /** * Get courses, in IFSC it is called "Turmas Virtuais". * @param allPeriods if true, all courses will be returned; otherwise, only latest courses. @@ -164,6 +244,8 @@ export class SigaaStudentBond implements StudentBond { 'SIGAA: Invalid courses table, could not find the column with class schedules.' ); } + + let coursesLocal = await this.getCoursesLocal(); let period; for (const row of rows) { @@ -177,6 +259,10 @@ export class SigaaStudentBond implements StudentBond { const [code, ...titleSlices] = fullname.split(' - '); const title = titleSlices.join(' - '); + let local; + for (const cl of coursesLocal) { + if (title === cl.title) local = cl.classLocal; + } const buttonCoursePage = cellElements .eq(tableColumnIndexs.button) .find('a[onclick]'); @@ -208,6 +294,7 @@ export class SigaaStudentBond implements StudentBond { title, code, schedule, + classLocal: local, numberOfStudents, period, id, diff --git a/src/courses/sigaa-course-student.ts b/src/courses/sigaa-course-student.ts index c3509bb..4d85742 100755 --- a/src/courses/sigaa-course-student.ts +++ b/src/courses/sigaa-course-student.ts @@ -50,6 +50,7 @@ export interface CourseStudentData { numberOfStudents: number; period: string; schedule: string; + classLocal?: string; form: SigaaForm; } @@ -84,6 +85,13 @@ export interface CourseStudent { */ readonly schedule: string; + /** + * Course class local. + * + * Local das aulas. + */ + readonly classLocal: string | undefined; + /** * Number of students, is 0 if the course of the period is not the current one. */ @@ -186,6 +194,11 @@ export class SigaaCourseStudent implements CourseStudent { */ readonly schedule; + /** + * @inheritdoc + */ + readonly classLocal; + /** * @inheritdoc */ @@ -217,6 +230,7 @@ export class SigaaCourseStudent implements CourseStudent { this.numberOfStudents = courseData.numberOfStudents; this.period = courseData.period; this.schedule = courseData.schedule; + this.classLocal = courseData.classLocal; this.form = courseData.form; this.resources = resourcesManagerFactory.createCourseResourcesManager( @@ -310,7 +324,7 @@ export class SigaaCourseStudent implements CourseStudent { let pageCourseCode: string | undefined; if (buttonLabel === 'Ver Notas') { - if (page.bodyDecoded.includes('Ainda não foram')) { + if (page.bodyDecoded.includes('Ainda não foram lançadas notas.')) { pageCourseCode = this.parser .removeTagsHtml(page.$('#linkCodigoTurma').html()) .replace(/ -$/, '');