Reewrite #1

Merged
Sirherobrine23 merged 2 commits from nearby-guanaco into main 2023-12-04 17:28:34 +00:00
9 changed files with 240 additions and 1300 deletions
Showing only changes of commit 5e853e9040 - Show all commits

@ -1,328 +1,16 @@
import { randomBytes } from "crypto"; import { ColaborareadRequest } from "./lib/colabora_req.js";
import { createReadStream, createWriteStream } from "fs";
import { mkdir, rm, writeFile } from "fs/promises";
import getVideoDuration from "get-video-duration";
import { Got, HTTPError } from "got";
import { JSDOM } from "jsdom";
import { tmpdir } from "os";
import path from "path";
import { pipeline } from "stream/promises";
import TurndownService from "turndown";
import { database } from "./lib/db_connect.js";
import { ActivityType, AtividadeRoot, BoletimColaborar } from "./lib/sync.js";
export type Activity = { export default Get;
/** ID da atividade */ export async function Get(gotDom: ColaborareadRequest["gotDom"], semestre: string) {
id: string; const dashboard = new URL("https://www.colaboraread.com.br/aluno/dashboard/index"); dashboard.searchParams.set("matriculaId", semestre);
description: string; const { url: pageUrl, jsdoc } = (await gotDom(dashboard));
} & ({ const { window: { document } } = jsdoc;
type: "Tele Aula";
file: { name: string; url: string; }[];
videoIDs: {
id: string;
exemplarId: string;
location: string;
videoDuration: number;
}[];
}|{
type: "Conteúdo Web";
files: {
title: string;
id: string;
url: string;
}[];
}|{
type: "Web Aula";
}|{
type: "Referências Digitais";
}|{
type: "Prova Presencial";
}|{
type: "Avaliação Virtual";
asks: (string[])|{
ask: string;
select: {
title: string;
value: string;
}[];
}[];
}|{
type: "Fórum";
}|{
type: "Portfólio";
files: { name: string; url: string; }[]
}|{
type: "Desafio Nota Máxima";
}|{
type: "Acelere Sua Carreira";
}|{
type: "Avaliação de Proficiência";
}|{
type: "Prova Prática Presencial";
}|{
type: "Atividade Diagnóstica";
}|{
type: "Atividade de Aprendizagem";
}|{
type: "Leitura";
files: { title: string; url: string }[];
externalFiles: { title: string; url: string }[];
});
export const activity = database.collection<Activity>("activity"); return Array.from(document.querySelectorAll("#navbar-content-aluno-pda > ul > li")).reduce<{ title: string; id: string }[]>((acc, e) => {
if (!(e.querySelector("table > tbody > tr > td:nth-child(1) > a"))) return acc;
export async function insertActivitysToCollection(got: Got, semestre: string, id: string) { const a = e.querySelector("table > tbody > tr > td:nth-child(1) > a");
// Mount URL Request's const title = a.getAttribute("title").trim(), pageHref = (new URL(a.getAttribute("href"), pageUrl)).searchParams.get("ofertaDisciplinaId")
const dashboard = new URL(path.posix.join("/aluno/timeline/index", semestre), "https://www.colaboraread.com.br"); dashboard.searchParams.set("ofertaDisciplinaId", id); if (pageHref) acc.push({ title, id: pageHref });
const index = await got(dashboard);
// Parse HTML Page
const doc = new JSDOM(index.body, { url: index.url });
const { window: { document } } = doc;
await mkdir("./tmp_data/", { recursive: true }).catch(() => {});
await writeFile(("./tmp_data/").concat(id, ".html"), index.body);
let filesIDs: string[] = [];
for (const e of document.querySelectorAll("#js-activities-container > li")) {
if (!(e.querySelector("div:nth-child(2) > div > h4 > span > span"))) continue;
const typeElement: HTMLSpanElement = e.querySelector("div:nth-child(2) > div > h4 > span > span");
Array.from(typeElement.querySelectorAll("*")).forEach(s => s.remove()); // Remove icon
const type = (typeElement.innerText || typeElement.innerHTML).trim().toLowerCase();
const titleElement: HTMLElement = e.querySelector("div.timeline-panel > div > h4 > small"), title = (titleElement.innerText || titleElement.innerHTML).trim();
if (type === "teleaula") {
const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
filesIDs.push(id);
if (await activity.findOne({ id })) continue;
const teleIndex = await got(new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url));
const doc = new JSDOM(teleIndex.body, { url: teleIndex.url });
const { window: { document } } = doc;
await activity.insertOne({
type: "Tele Aula",
id,
description: title,
file: Array.from(e.querySelectorAll("div.timeline-panel > div > div.timeline-body > ul > li")).filter(a => !!(a.querySelector("small > a"))).map(a => a.querySelector("small > a")).map(s => ({ name: s.getAttribute("title"), url: (new URL(s.getAttribute("href"), index.url)).toString() })),
videoIDs: (await Promise.all(Array.from(document.querySelectorAll("#conteudo > div > div > div.col-md-8.panel.mb-30 > div:nth-child(2) > div > div > div")).map(s => s.querySelector("div > div > a").getAttribute("onclick")).map(async (click) => {
click = click.split("\n").map(s => s.trim()).join("").trim();
click = click.slice(click.indexOf("(")+1, click.indexOf(")"));
const params = click.split(",").map(s => s.trim()).map(s => s.slice(s.startsWith("\"") ? 1 : 0, s.endsWith("\"") ? -1 : undefined).trim());
const [ codigoMidia, , , atividadeOfertaId, exemplarId ] = params;
const fileUrl = (new URL(path.posix.join("/video", codigoMidia), "https://mdstrm.com")).toString().concat(".mp4");
const videoDurationURL = new URL("https://colaboraread.com.br/aluno/playerVideo/getVideoDuration");
videoDurationURL.searchParams.set("codigoMidia", codigoMidia);
const videoDuration: number = await got(videoDurationURL).then(s => JSON.parse(s.body).videoDuration, async (err: HTTPError) => {
if (err.response?.statusCode === 500) {
const file = path.join(tmpdir(), randomBytes(8).toString("hex").concat(".mp4"));
return pipeline(got(fileUrl, { isStream: true }), createWriteStream(file)).then(() => getVideoDuration.getVideoDurationInSeconds(createReadStream(file)), (err: HTTPError) => {
if (err.response && err.response.statusCode === 404) return undefined;
throw err;
}).finally(() => rm(file, { force: true }));
}
throw err;
});
if (!videoDuration) return null;
return {
id: atividadeOfertaId,
exemplarId,
location: fileUrl,
videoDuration
};
}))).filter(Boolean),
});
} else if (type === "portfólio") {
const id = e.querySelector("div.timeline-panel > div > p:nth-child(5) > small > em").textContent;
filesIDs.push(id);
if (await activity.findOne({ id })) continue;
await activity.insertOne({
type: "Portfólio",
id,
description: title,
files: Array.from(e.querySelectorAll("div.timeline-panel > div > div.timeline-body > ul > li")).map(e => e.querySelector("small > a")).filter(Boolean).map(s => ({ name: s.getAttribute("title"), url: (new URL(s.getAttribute("href"), index.url)).toString() }))
});
} else if (type === "leitura") {
const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
filesIDs.push(id);
if (await activity.findOne({ id })) continue;
await activity.insertOne({
type: "Leitura",
id, description: title,
files: Array.from(e.querySelectorAll("div.timeline-panel > div > div > ul:nth-child(3) > li")).map(s => s.querySelector("small > a")).filter(Boolean).map(s => ({title: s.getAttribute("title"), url: (new URL(s.getAttribute("href"), index.url)).toString() })),
externalFiles: Array.from(e.querySelectorAll("div.timeline-panel > div > div > ul:nth-child(7) > li")||[]).map(s => s.querySelector("small > a")).filter(Boolean).map(s => ({ title: s.getAttribute("title")||s.innerHTML.trim(), url: (new URL(s.getAttribute("href"), index.url)).toString() })),
});
} else if (type === "conteúdo web") {
const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
filesIDs.push(id);
if (await activity.findOne({ id })) continue;
const webExtIndex = await got(new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url));
const doc = new JSDOM(webExtIndex.body, { url: webExtIndex.url });
const { window: { document } } = doc;
if (document.querySelector("#detalhe > summary > p > b")) {
await activity.insertOne({
type: "Conteúdo Web",
id, description: title,
files: Array.from(document.querySelectorAll("#detalhe > a")).map(s => {
const page = s.getAttribute("href");
const atividadeOfertaId = ((s.getAttribute("onclick")||"").trim().split("\n").map(s => s.trim()).join("")).slice((s.getAttribute("onclick")||"").indexOf("(")+1, (s.getAttribute("onclick")||"").indexOf(")")).split(",").map(s => ((s = s.trim()).startsWith("\"")||s.startsWith("'")) ? s.slice(1, -1) : s)[0];
return {
title: s.querySelector("p").textContent.trim(),
id: atividadeOfertaId,
url: (new URL(page, webExtIndex.url)).toString(),
};
}),
});
} else throw new Error(doc.serialize());
} else if (type === "avaliação virtual") {
const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
filesIDs.push(id);
if (await activity.findOne({ id })) continue;
const avaIndex = await got(new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url));
const doc = new JSDOM(avaIndex.body, { url: avaIndex.url });
const { window: { document } } = doc;
// Atividade não liberada!
if (!(document.querySelector("#conteudo > div.container-fluid > div:nth-child(2) > div.col-md-9 > div"))) continue;
await activity.insertOne({
type: "Avaliação Virtual",
id, description: title,
asks: Array.from(document.querySelectorAll("#conteudo > div.container-fluid > div:nth-child(2) > div.col-md-9 > div")).map(s => s.querySelector("div")).map(s => {
Array.from(s.querySelectorAll("*")).filter(s => s.getAttribute("href")||s.getAttribute("src")).forEach(s => {
if (s.getAttribute("href")) s.setAttribute("href", (new URL(s.getAttribute("href"), avaIndex.url)).toString());
else s.setAttribute("src", (new URL(s.getAttribute("src"), avaIndex.url)).toString());
});
return (new TurndownService()).turndown(s.innerHTML.trim());
})
});
} else if (type === "live") {
// const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
} else if (type === "engajamento ava") {
// const id = e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").textContent;
} else {
const id = String(Array.from(e.querySelectorAll("* > p > small")).filter(s => s.textContent.toLowerCase().includes("cod")||s.textContent.toLowerCase().includes("atividade:")).map(s => (s.querySelector("em").textContent)).at(-1)||"").trim();
if (!id) continue;
console.dir({ type, title, id }, {depth: null});
}
}
return filesIDs;
}
export async function Boletim(got: Got, matricula: string, disciplina: string) {
const botURL = new URL(path.posix.join("/aluno/boletim/getListaDetalheBoletimDto"), "https://colaboraread.com.br/");
botURL.searchParams.set("matriculaId", matricula);
botURL.searchParams.set("ofertaDisciplinaId", disciplina); // botURL.searchParams.set("tipoDisciplinaId", "13");
const boletimRequest = await got(botURL);
// Pretty boletim
const boletim = (JSON.parse(boletimRequest.body) as BoletimColaborar[]).map(fn => {
return {
ava: fn.detalheBoletimDto.detalheEngajamentoDtoList.reduce<Record<string, number>>((acc, { descricaoAtividade: name, pontuacao }) => { acc[name] = pontuacao; return acc; }, {}),
frenquencia: Object.keys(fn.detalheBoletimDto.detalheFrequenciaDtoList).reduce<{type: ActivityType; descricaoAtividade: string; situacao?: string; cargaHoraria: { cargaHorariaDisciplina: number; cargaHoraria: number; }; datas: { lancamento: string; atividade: string; inicio: string; fim: string; }; tipo: { id: number; atividade: string; };}[]>((acc, id) => {
const atividade = fn.detalheBoletimDto.detalheFrequenciaDtoList[id];
let ActType: ActivityType = "Não definido";
if (id === "2") ActType = "Tele Aula";
else if (id === "4") ActType = "Web Aula";
else if (id === "17") ActType = "Conteúdo Web";
else if (id === "14") ActType = "Referências Digitais";
else if (id === "13") ActType = "Prova Presencial";
else if (id === "6") ActType = "Avaliação Virtual";
else if (id === "16") ActType = "Fórum";
else if (id === "1") ActType = "Portfólio";
else if (id === "28") ActType = "Desafio Nota Máxima";
else if (id === "31") ActType = "Acelere Sua Carreira";
else if (id === "22") ActType = "Avaliação de Proficiência";
else if (id === "26") ActType = "Prova Prática Presencial";
else if (id === "23") ActType = "Atividade Diagnóstica";
else if (id === "24") ActType = "Atividade de Aprendizagem";
return acc.concat(atividade.map((s): (typeof acc)[number] => ({
type: ActType,
descricaoAtividade: s.descricaoAtividade,
situacao: s.situacao,
cargaHoraria: {
cargaHorariaDisciplina: s.cargaHorariaDisciplina,
cargaHoraria: s.cargaHoraria,
},
datas: {
lancamento: s.dataLancamento,
atividade: s.dataAtividade,
inicio: s.dataInicioAtividade,
fim: s.dataFimAtividade,
},
tipo: {
id: s.tipoOfertaId,
atividade: s.tipoAtividade.name,
}
})));
}, []),
atividades: Object.keys(fn.detalheBoletimDto.detalheDisciplinarDtoList).reduce<AtividadeRoot[]>((acc, id) => {
const atividade = fn.detalheBoletimDto.detalheDisciplinarDtoList[id];
let ActType: Exclude<ActivityType, "Portfólio"|"Prova Presencial"> = "Não definido";
if (id === "1") return acc.concat(atividade.map(({ descricaoAtividade, atividadeId, atividadeOfertaId, pontuacaoMinina, pontuacao }): (typeof acc)[number] => ({ type: "Portfólio", atividadeId, atividadeOfertaId, descricaoAtividade, pontuacaoMinina, pontuacao })));
else if (id === "2") ActType = "Tele Aula";
else if (id === "4") ActType = "Web Aula";
else if (id === "6") ActType = "Avaliação Virtual";
else if (id === "13") return acc.concat(atividade.map(({ descricaoAtividade, atividadeId, atividadeOfertaId, pontuacaoMinina, pontuacao, acertos, sequenciaAvaliacao }): (typeof acc)[number] => ({ type: "Prova Presencial", atividadeId, atividadeOfertaId, descricaoAtividade, acertos, sequenciaAvaliacao, pontuacaoMinina, pontuacao })));
else if (id === "14") ActType = "Referências Digitais";
else if (id === "16") ActType = "Fórum";
else if (id === "17") ActType = "Conteúdo Web";
else if (id === "22") ActType = "Avaliação de Proficiência";
else if (id === "23") ActType = "Atividade Diagnóstica";
else if (id === "24") ActType = "Atividade de Aprendizagem";
else if (id === "26") ActType = "Prova Prática Presencial";
else if (id === "28") ActType = "Desafio Nota Máxima";
else if (id === "31") ActType = "Acelere Sua Carreira";
// Generic
return acc.concat(atividade.map((s): (typeof acc)[number] => {
const { descricaoAtividade, atividadeAvaliacaoId, atividadeOfertaId, atividadeId, sequenciaAvaliacao, pontuacaoMinina, pontuacao } = s;
return {
type: ActType,
descricaoAtividade,
atividadeAvaliacaoId, atividadeOfertaId, atividadeId,
pontos: {
sequenciaAvaliacao,
pontuacaoMinina,
pontuacao,
},
raw: s,
}
}));
}, []),
// atividadesRaw: fn.detalheBoletimDto.detalheDisciplinarDtoList,
};
});
// Media final calc
const Pontos: number[] = []
// Provas
const provas = boletim[0].atividades.reduce((acc, i) => i.type === "Prova Presencial" ? (acc + i.pontuacao) : acc, 0);
Pontos.push(provas);
let avaPoints = Object.keys(boletim[0].ava).reduce((acc, k) => acc + boletim[0].ava[k], 0);
let avaliacao = boletim[0].atividades.reduce((acc, info) => {
if (info.type !== "Avaliação Virtual") return acc;
info.raw.sequenciaAvaliacao === 1 && acc.g < info.raw.pontuacao && (acc.g = info.raw.pontuacao);
info.raw.sequenciaAvaliacao === 2 && acc.a < info.raw.pontuacao && (acc.a = info.raw.pontuacao);
acc.k = acc.g + acc.a;
return acc; return acc;
}, { k: 0, g: 0, a: 0 }).k; }, []);
if (provas >= 3000 && avaliacao >= 1500) Pontos.push(avaliacao, avaPoints);
const media = parseFloat(((Pontos.reduce((acc, p) => acc+p, 0) / 100) / 10).toPrecision(2));
return {
/** Media da aula, nota não final */
media: {
nota: media,
aprovado: media >= 6,
},
boletim
};
} }

119
src/boletim.ts Normal file

@ -0,0 +1,119 @@
import { Got } from "./lib/colabora_req.js";
export enum AulasType {
"Portfólio" = "1",
"Tele Aula" = "2",
"Web Aula" = "4",
"Avaliação Virtual" = "6",
"Prova Presencial" = "13",
"Referências Digitais" = "14",
"Fórum" = "16",
"Conteúdo Web" = "17",
"Avaliação de Proficiência" = "22",
"Atividade Diagnóstica" = "23",
"Atividade de Aprendizagem" = "24",
"Prova Prática Presencial" = "26",
"Desafio Nota Máxima" = "28",
"Acelere Sua Carreira" = "31",
};
export type Boletim = {
detalheBoletimDto: {
detalheEngajamentoDtoList: {
descricaoAtividade: string;
pontuacao: number;
tipoAtividade: {
enumType: string;
name: string;
}
}[];
detalheFrequenciaDtoList: Record<string, {
cargaHoraria: number;
cargaHorariaDisciplina: number;
dataAtividade: string;
dataFimAtividade: string;
dataInicioAtividade: string;
dataLancamento: string;
descricaoAtividade: string;
situacao: string;
tipoOfertaId: number;
tipoAtividade: {
enumType: string;
name: string;
};
}[]>;
detalheDisciplinarDtoList: Record<AulasType, {
acertos: number|null;
apresentacao: boolean;
atividadeAvaliacaoId: number;
atividadeId: number;
atividadeLancamentoId?: number;
atividadeOfertaId: number;
caminhoAtividade1: any;
caminhoAtividade2?: any;
dataAtividade1: string;
dataAtividade2: string;
dataFimGabaritoAluno: string;
dataInicioGabaritoAluno: string;
descricaoAtividade: string;
gabarito: any;
interdisciplinar: boolean;
likes: number;
objetivas?: string;
pontuacao: number;
pontuacaoApresentacao: number;
pontuacaoMinina: number;
pontuacaoTotalApresentacao: number;
sequenciaAvaliacao?: number;
tipoAtividadeId: number;
tipoOfertaId: number;
tipoAtividade: {
enumType: string;
name: string;
};
}[]>;
}
}[];
export default Get;
export async function Get(got: Got, matricula: string, disciplina: string) {
const boletimUrl = new URL("https://www.colaboraread.com.br/aluno/boletim/getListaDetalheBoletimDto");
boletimUrl.searchParams.set("matriculaId", matricula);
boletimUrl.searchParams.set("ofertaDisciplinaId", disciplina);
const body: Boletim = (await got<Boletim>(boletimUrl, { responseType: "json" })).body;
const media = body.map(body => {
const Acts = Object.keys(body.detalheBoletimDto.detalheDisciplinarDtoList).map<{type: AulasType, pontos: number}>((type: AulasType) => {
if (type === AulasType["Avaliação Virtual"]) {
return {
type,
pontos: body.detalheBoletimDto.detalheDisciplinarDtoList[AulasType["Avaliação Virtual"]].reduce((acc, info) => {
info.sequenciaAvaliacao === 1 && acc.g < info.pontuacao && (acc.g = info.pontuacao);
info.sequenciaAvaliacao === 2 && acc.a < info.pontuacao && (acc.a = info.pontuacao);
acc.k = acc.g + acc.a;
return acc;
}, { k: 0, g: 0, a: 0 }).k,
}
}
return {
type,
pontos: body.detalheBoletimDto.detalheDisciplinarDtoList[type].reduce<number>((pontos, info) => pontos + info.pontuacao, 0),
}
}).reduce<Record<AulasType, number>>((acc, k) => Object.assign(acc, { [k.type]: k.pontos }), Object.create({}));
const ava = body.detalheBoletimDto.detalheEngajamentoDtoList.reduce((acc, k) => acc + k.pontuacao, 0);
const Notas: (number|undefined)[] = [ Acts[AulasType["Prova Presencial"]], Acts[AulasType["Portfólio"]], ava ];
if (Acts[AulasType["Prova Presencial"]] >= 3000) Notas.push(ava, Acts[AulasType["Avaliação Virtual"]]);
const media = parseFloat(Math.min(10, Math.max(0, ((Notas.reduce((acc, p) => acc + (p||0), 0) / 100) / 10))).toPrecision(2));
return {
media,
aprovado: media >= 6,
};
});
return {
media,
boletim: body,
};
}

23
src/dashboard.ts Normal file

@ -0,0 +1,23 @@
import { ColaborareadRequest } from "./lib/colabora_req.js";
export default Get;
export async function Get(gotdom: ColaborareadRequest["gotDom"]) {
const { window: { document } } = (await gotdom("https://www.colaboraread.com.br/index/index")).jsdoc;
document.querySelector("#navbar-content-aluno-cursos > div > div:nth-child(1) > div")
return Array.from(document.querySelectorAll("#navbar-content-aluno-cursos > div > div")).reduce<{ name: string; semestres: {semestreID: string; title: string;}[] }[]>((acc, e) => {
if (!(e.querySelector("div > div:nth-child(2) > h3"))) return acc;
else if (!(e.querySelector("div > div:nth-child(1) > div:nth-child(3) > form > div:nth-child(1) > select > option"))) return acc;
// Title
const title = (e.querySelector("div > div:nth-child(2) > h3") as HTMLHeadingElement).getAttribute("title");
// Push to array
acc.push({
name: title,
semestres: Array.from<HTMLOptionElement>(e.querySelectorAll("div > div:nth-child(1) > div:nth-child(3) > form > div:nth-child(1) > select > option")).map(({ value, innerText, innerHTML }) => ({ semestreID: value, title: (innerText||innerHTML).trim() })).reverse()
});
// retornar os cursos
return acc;
}, []);
}

@ -1,261 +1,78 @@
import neste, { parseBody } from "neste"; import "./lib/db_connect.js";
import crypto from "node:crypto"; import request, { ColaborareadRequest } from "./lib/colabora_req.js";
import { tmpdir } from "node:os"; import { encrypt } from "./lib/password.js";
import path from "node:path"; import neste from "neste";
import { Boletim, activity } from "./atividades.js"; import boletim from "./boletim.js";
import { remoteRequest } from "./lib/colabora_req.js"; import Dashboard from "./dashboard.js";
import { decrypt, encrypt } from "./lib/password.js"; import Atividade from "./atividades.js";
import "./telegram.js";
import { User, syncUser, user } from "./user.js";
const app = neste(); const app = neste();
// Listen HTTP Server
app.listen(process.env.PORT||"3000", () => console.log(app.address()));
declare module "neste" { declare module "neste" {
export interface Request { export interface Request {
User: User; Got?: ColaborareadRequest;
} }
} }
app.post("/login", parseBody(), async (req, res) => { app.use(async (req, res, next): Promise<any> => {
const { username, password } = req.body; if (typeof req.headers.authorization === "string" && req.headers.authorization.length > 8) {
const dbUser = await user.findOne({ "colaborar.user": username }); if (req.headers.authorization.toLowerCase().startsWith("basic ")) {
if (!dbUser) return res.status(400).json({ errors: [ { type: "user", message: "Usuario não foi cadastrado no nosso sistema" } ] });
else if (password !== await decrypt(dbUser.colaborar.password)) return res.status(400).json({ errors: [ { type: "passwd", message: "Senha invalida!" } ] });
let sessionid: string;
while (!sessionid) {
sessionid = crypto.randomBytes(16).toString("hex");
if (await user.findOne({ sessions: sessionid })) sessionid = undefined;
}
await user.findOneAndUpdate({ userID: dbUser.userID }, { $push: { sessions: sessionid } });
req.Cookies.set("sessionid", sessionid);
return res.status(200).json({ sessionid });
});
app.post("/register", parseBody(), async (req, res) => {
if (!req.body) return res.status(400).json({ errors: [ { type: "body", message: "require body!" } ] });
const { username, password } = req.body;
if (!(typeof username === "string" && typeof password === "string")) return res.status(400).json({ errors: [ { type: "body", message: "Invalid body!" }, { type: "username", message: "require string username" }, { type: "password", message: "require string password" } ] });
else if (await user.findOne({ "colaborar.user": username })) return res.status(400).json({ errors: [ { type: "user", message: "Usuario jã foi cadastrado no nosso sistema" } ] });
try { try {
// Try login let password: string, username: string;
await remoteRequest(username, await encrypt(password)); const decode = Buffer.from(req.headers.authorization.slice(6).trim(), "base64").toString("utf8");
if (decode.indexOf(":") > 0) {
// UUID unique const i = decode.indexOf(":");
let userID: string; username = decode.slice(0, i);
while (!userID) { password = decode.slice(i+1);
userID = crypto.randomUUID();
if (await user.findOne({ userID })) userID = undefined;
} }
req.Got = await request(username, await encrypt(password));
let sessionid: string; return next();
while (!sessionid) {
sessionid = crypto.randomBytes(16).toString("hex");
if (await user.findOne({ sessions: sessionid })) sessionid = undefined;
}
// Insert in db
await user.insertOne({
userID,
colaborar: {
user: username,
password: await encrypt(password)
},
telegramChats: [],
sessions: [ sessionid ],
aulas: {}
});
req.Cookies.set("sessionid", sessionid);
return res.status(201).json({ userID, sessionid });
} catch (err) { } catch (err) {
return res.status(401).json({ return res.status(400).json({
errors: [ errors: [
{ {
type: "auth", type: "Auth",
message: "Não foi possivel autenticar no site do colaboraread, cheque seu login" message: "Usuario ou senha Errados"
}, },
{ {
type: "generic", type: "colaborareadAuth",
message: err.message||String(err) message: err?.message||String(err)
} }
] ]
}); });
} }
}); } else if (req.headers.authorization.toLowerCase().startsWith("token ")) {
req.headers.authorization.slice(6).trim();
app.use(async (req, res, next) => {
let { authorization } = req.headers;
if (req.Cookies.has("sessionid")) {
req.User = await user.findOne({ sessions: req.Cookies.get("sessionid")[0] });
if (req.User) return next();
} }
if (typeof authorization === "string") {
if (authorization.startsWith("token ")||authorization.startsWith("Token ")) {
authorization = authorization.substring(5).trim();
req.User = await user.findOne({ sessions: authorization });
} else if (authorization.startsWith("basic ")||authorization.startsWith("Basic ")) {
const decodeAuth = Buffer.from(authorization.substring(5).trim(), "base64").toString("utf8");
const findex = decodeAuth.indexOf(":"), username = decodeAuth.substring(0, findex-1), password = decodeAuth.substring(findex);
req.User = await user.findOne({ "colaborar.user": username });
if (req.User && (await decrypt(req.User.colaborar.password) !== password)) req.User = undefined;
}
if (req.User) return next();
} }
return res.status(401).json({ return res.status(401).json({
errors: [ errors: [
{ {
type: "auth", type: "Auth",
message: "Require Cookie sessionid or Authorization header!" message: "Usuario ou token necessario"
} }
] ]
}); });
}, parseBody({ formEncoded: { limit: -1, limitResponse: false }, formData: { tmpFolder: path.join(tmpdir(), String().concat("colaborar_data")) } }));
// Cursos
app.get("/", async (req, res) => {
return res.json(Object.keys(req.User.aulas).reduce((acc, semestre) => acc.concat({ semestre, activity: Object.keys(req.User.aulas[semestre]).map(id => ({ name: req.User.aulas[semestre][id].name, id })) }), []));
}); });
// Sincronizar as aulas com o banco de dados // Semestres
app.post("/", async (req, res) => { app.get("/", async (req, res) => res.json(await Dashboard(req.Got.gotDom)));
const ids = await syncUser(req.User.userID);
return res.status(201).json(ids.aulas);
});
// Check
app.use("/:matricula", async (req, res, next) => req.User.aulas[req.params.matricula] ? next() : res.status(404).json({ errors: [ { type: "Matricula", message: "Matricula não registrada!" } ] }));
// Aulas // Aulas
app.get("/:matricula", async (req, res) => res.json(Object.keys(req.User.aulas[req.params.matricula]).map(id => ({name: req.User.aulas[req.params.matricula][id].name, id})))); app.get("/aula/:semestre", async (req, res) => res.json(await Atividade(req.Got.gotDom, req.params.semestre)));
app.get("/aula/:semestre/:displina");
// Sync semestre // Boletim
app.post("/:matricula/sync", async (req, res) => { app.get("/boletim", async (req, res) => {
const ids = await syncUser(req.User.userID, req.params.matricula); res.json(await Promise.all((await Dashboard(req.Got.gotDom)).map(async s => ({ name: s.name, semestres: await Promise.all(s.semestres.map(async s => await Promise.all((await Atividade(req.Got.gotDom, s.semestreID)).map(async s => ({ title: s.title, boletim: await boletim(req.Got.got, req.params.semestre, s.id), }))))) }))));
return res.status(201).json(ids.aulas[req.params.matricula]);
}); });
app.get("/boletim/:semestre", async (req, res) => {
// Get activity's res.json(await Promise.all((await Atividade(req.Got.gotDom, req.params.semestre)).map(async s => ({ title: s.title, boletim: await boletim(req.Got.got, req.params.semestre, s.id), }))));
app.get("/:matricula/:disciplina", async (req, res, next) => {
if (!(req.User.aulas[req.params.matricula][req.params.disciplina])) return next();
const { name, ids } = req.User.aulas[req.params.matricula][req.params.disciplina];
return res.json({
title: name,
activity: await activity.find({ id: { $in: ids } }).toArray(),
});
}); });
app.get("/boletim/:semestre/:displina", async (req, res) => {
app.get("/:matricula/:disciplina/book", async (req, res, next) => { const bot = await boletim(req.Got.got, req.params.semestre, req.params.displina);
if (!(req.User.aulas[req.params.matricula][req.params.disciplina])) return next(); return res.json(bot);
const { ids } = req.User.aulas[req.params.matricula][req.params.disciplina];
const act = await activity.find({ id: { $in: ids } }).toArray();
const book = act.filter(s => s.type === "Leitura" && (s.files.concat(s.externalFiles).some(x => x.title.toLowerCase().includes("livro")||x.title.toLowerCase().includes("didático")))).at(0);
if (!book || book.type !== "Leitura") return res.status(400).json({ erros: [ { type: "file", message: "File not found!" } ] });
const fileUrl = new URL(book.files.concat(book.externalFiles).find(x => x.title.toLowerCase().includes("livro")||x.title.toLowerCase().includes("didático")).url);
const { got } = await remoteRequest(req.User.colaborar.user, req.User.colaborar.password);
if (fileUrl.searchParams.has("urlOrigem")) return got(fileUrl.searchParams.get("urlOrigem"), { isStream: true }).pipe(res);
return got(fileUrl, { isStream: true }).pipe(res);
}); });
app.get("/:matricula/:disciplina/teleaula/:id", async (req, res) => {
const act = await activity.findOne({ id: req.params.id });
if (!act || act.type !== "Tele Aula") return res.status(400).json({ error: [ { type: "activity", message: "Aula não existe" } ].concat(act.type !== "Tele Aula" ? { type: "activity", message: "ID invalido" } : undefined) });
const { got } = await remoteRequest(req.User.colaborar.user, req.User.colaborar.password);
const videos = await Promise.all(act.videoIDs.map(async d => {
const currentTime: number = JSON.parse((await got("https://www.colaboraread.com.br/aluno/videoAnotacao/getStatusExemplar", {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
body: (new URLSearchParams({
matriculaId: req.params.matricula,
exemplarId: d.exemplarId
})).toString()
})).body).currentTime;
return Object.assign(d, { currentTime });
}))
return res.json({
title: act.description,
videos,
files: act.file.map(s => ({
title: s.name,
download: path.posix.join(req.reqPath, Buffer.from(s.url, "utf8").toString("hex")),
})),
});
});
app.post("/:matricula/:disciplina/teleaula/:id", async (req, res) => {
if (!req.body) return res.status(400).json({ errors: [ { type: "body", message: "require body!" } ] });
const { videoID, time, end = false } = req.body;
if (!(typeof videoID === "string" && time > 0)) return res.status(400).json({ errors: [ { type: "body", message: "require body!" } ] });
const act = await activity.findOne({ id: req.params.id });
if (!act || act.type !== "Tele Aula") return res.status(400).json({ error: [ { type: "activity", message: "Aula não existe" } ].concat(act.type !== "Tele Aula" ? { type: "activity", message: "ID invalido" } : undefined) });
const video = act.videoIDs.find(s => s.id === videoID);
if (!video) return res.status(400).json({ errors: [ { type: "videoID", message: "Video ID not found!" } ] });
const { got } = await remoteRequest(req.User.colaborar.user, req.User.colaborar.password);
if (end) {
await got("https://www.colaboraread.com.br/aluno/videoAnotacao/updateEndTimeStatusExemplarVideo", {
headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: (new URLSearchParams({
currentTime: "0", end: "true", // Valores fixos
matriculaId: req.params.matricula,
exemplarId: video.exemplarId,
})).toString(),
});
} else {
await got("https://www.colaboraread.com.br/aluno/playerVideo/updateProgressoAluno", {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
body: (new URLSearchParams({
matriculaId: req.params.matricula,
atividadeOfertaId: video.id,
totalTime: String(video.videoDuration),
currentTime: String(time),
})).toString(),
});
}
return res.status(201).json({
atividadeOfertaId: video.id,
totalTime: video.videoDuration,
currentTime: time,
});
});
app.get("/:matricula/:disciplina/teleaula/:id/:filehash", async (req, res) => {
const act = await activity.findOne({ id: req.params.id });
if (!act || act.type !== "Tele Aula") return res.status(400).json({ error: [ { type: "activity", message: "Aula não existe" } ].concat(act.type !== "Tele Aula" ? { type: "activity", message: "ID invalido" } : undefined) });
const fileUrl = Buffer.from(req.params.filehash, "hex").toString("utf8");
const is = act.file.find(s => s.url === fileUrl);
if (!is) return res.status(404).json({ errors: [ { type: "file", message: "File not found!" } ] });
const { got } = await remoteRequest(req.User.colaborar.user, req.User.colaborar.password);
return got(fileUrl, { isStream: true }).pipe(res);
});
app.get("/:matricula/:disciplina/boletim", async (req, res) => {
const { got } = await remoteRequest(req.User.colaborar.user, req.User.colaborar.password);
return res.json(await Boletim(got, req.params.matricula, req.params.disciplina));
});
app.get("/:matricula/:disciplina/:id", async (req, res) => {
const { matricula, disciplina, id } = req.params;
if (!(req.User.aulas[matricula][disciplina].ids.includes(id))) return res.status(404).json({ errors: [ { type: "material", message: "Conteudo não liberado" } ] });
const act = await activity.findOne({ id });
return res.json(act);
});
app.use((req, res) => {
console.log(req.path);
res.sendStatus(404);
});
// Listen
app.listen(process.env.PORT||3000, () => { const addr = app.address(); console.log("HTTP Listen on %s", typeof addr === "object" ? addr.port : addr); })

@ -1,15 +1,29 @@
import { decrypt } from "./password.js"; import { decrypt } from "./password.js";
import { JSDOM } from "jsdom";
import cookie from "cookie"; import cookie from "cookie";
import got from "got"; import got, { Got, OptionsOfBufferResponseBody, OptionsOfTextResponseBody } from "got";
import { IncomingHttpHeaders } from "http";
export {remoteRequest}; export type { Got };
export { remoteRequest };
export interface ColaborareadRequest {
cookies: Map<string, Record<string, string>>;
reloadAuth: () => Promise<void>;
got: Got;
gotDom: (url: string | URL, options?: OptionsOfTextResponseBody | OptionsOfBufferResponseBody) => Promise<{
url: string;
headers: IncomingHttpHeaders;
jsdoc: JSDOM;
}>;
}
/** /**
* Create extends to got * Create extends to got
* @param username - Username to auth * @param username - Username to auth
* @param password - Password to auth * @param password - Password to auth
*/ */
export default async function remoteRequest(username: string, password: string) { export default async function remoteRequest(username: string, password: string): Promise<ColaborareadRequest> {
password = await decrypt(password); password = await decrypt(password);
const localCookie = new Map<string, Record<string, string>>() const localCookie = new Map<string, Record<string, string>>()
const defaultGot = got.extend({ const defaultGot = got.extend({
@ -60,11 +74,22 @@ export default async function remoteRequest(username: string, password: string)
const localURL = new URL((auth.headers.location || "/"), "https://www.colaboraread.com.br"); const localURL = new URL((auth.headers.location || "/"), "https://www.colaboraread.com.br");
if (auth.statusCode === 302 && (localURL.pathname === "/login/authfail" || localURL.pathname === "/login/auth" || localURL.searchParams.has("login_error"))) throw new Error("Invalid username or password!"); if (auth.statusCode === 302 && (localURL.pathname === "/login/authfail" || localURL.pathname === "/login/auth" || localURL.searchParams.has("login_error"))) throw new Error("Invalid username or password!");
} }
await reloadAuth(); await reloadAuth();
const goto = defaultGot.extend({ followRedirect: true });
async function gotDom(url: string|URL, options?: OptionsOfTextResponseBody|OptionsOfBufferResponseBody) {
const req = await goto(url, options);
return {
url: req.url,
headers: req.headers,
jsdoc: new JSDOM(req.body as string|Buffer, { url: req.url, }),
};
}
return { return {
got: defaultGot.extend({ followRedirect: true }),
cookies: localCookie, cookies: localCookie,
reloadAuth reloadAuth,
got: goto,
gotDom,
}; };
} }

@ -2,6 +2,7 @@ import crypto from "node:crypto";
import util from "node:util"; import util from "node:util";
const scrypt = util.promisify(crypto.scrypt) as (password: crypto.BinaryLike, salt: crypto.BinaryLike, keylen: number) => Promise<Buffer>; const scrypt = util.promisify(crypto.scrypt) as (password: crypto.BinaryLike, salt: crypto.BinaryLike, keylen: number) => Promise<Buffer>;
export async function encrypt(text: string): Promise<string> { export async function encrypt(text: string): Promise<string> {
const iv = crypto.randomBytes(16), secret = crypto.randomBytes(24); const iv = crypto.randomBytes(16), secret = crypto.randomBytes(24);
const key = await scrypt(secret, "salt", 24); const key = await scrypt(secret, "salt", 24);

@ -1,540 +0,0 @@
import { Got } from "got";
import { JSDOM } from "jsdom";
import path from "path";
export async function Cursos(got: Got) {
const index = await got("https://www.colaboraread.com.br/index/index");
const doc = new JSDOM(index.body, { url: index.url });
const { window: { document } } = doc;
document.querySelector("#navbar-content-aluno-cursos > div > div:nth-child(1) > div")
return Array.from(document.querySelectorAll("#navbar-content-aluno-cursos > div > div")).reduce<{ name: string; semestres: {semestreID: string; title: string;}[] }[]>((acc, e) => {
if (!(e.querySelector("div > div:nth-child(2) > h3"))) return acc;
else if (!(e.querySelector("div > div:nth-child(1) > div:nth-child(3) > form > div:nth-child(1) > select > option"))) return acc;
// Title
const title = (e.querySelector("div > div:nth-child(2) > h3") as HTMLHeadingElement).getAttribute("title");
// Push to array
acc.push({
name: title,
semestres: Array.from<HTMLOptionElement>(e.querySelectorAll("div > div:nth-child(1) > div:nth-child(3) > form > div:nth-child(1) > select > option")).map(({ value, innerText, innerHTML }) => ({ semestreID: value, title: (innerText||innerHTML).trim() })).reverse()
});
// retornar os cursos
return acc;
}, []);
}
export type Prova = {
semestre: string;
ano: string;
unidade: {
nome: string;
unidadeCode: string;
};
exames: {
nome: string;
code: string;
cancelado: boolean;
status: number;
disciplina: {
nome: string;
codigo: string;
};
dates: {
start: Date;
end: Date;
schedule?: {
start: Date;
end: Date;
}
};
gabarito?: any;
}[];
};
export async function Aulas(got: Got, semestre: string) {
const dashboard = new URL("https://www.colaboraread.com.br/aluno/dashboard/index"); dashboard.searchParams.set("matriculaId", semestre);
const index = await got(dashboard);
const doc = new JSDOM(index.body, { url: index.url });
const { window: { document } } = doc;
const provaURL = Array.from(document.querySelectorAll("a")).filter(s => s.href.includes("/redirectAgendamentoProvaFrontend"))[0]?.href;
let prova: undefined|Prova;
if (provaURL) {
try {
const redirectAgen = new URL(provaURL, index.url);
const { headers: { location: tokenAgendamento } } = await got(redirectAgen, { followRedirect: false });
const token = (new URL(tokenAgendamento)).searchParams.get("token");
const provasBody = (await got("https://x47kn51jsc.execute-api.us-east-1.amazonaws.com/application/student/exams/", { headers: { authorization: ("Bearer ").concat(token), }, responseType: "json"})).body as any;
prova = {
ano: redirectAgen.searchParams.get("ano"),
semestre: provasBody.semester,
unidade: provasBody.unit,
exames: await Promise.all((provasBody.exams as any[]).map(async (s): Promise<Prova["exames"][number]> => ({
nome: s.discipline,
code: s.locator,
status: s.status,
cancelado: s.canceled,
disciplina: {
codigo: s.disciplineCod,
nome: s.title,
},
dates: {
start: new Date(s.beginDate),
end: new Date(s.endDate),
...((s.beginScheduler && s.endScheduler) && {
schedule: {
start: new Date(s.beginScheduler),
end: new Date(s.endScheduler)
}
})
},
gabarito: ((new Date(s.feedback?.endDate||Date.now() + 1000)).getTime() >= Date.now()) ? (await got(("https://yvlfaxjs8l.execute-api.us-east-1.amazonaws.com/prd/feedback?locator=").concat(s.locator), { headers: { authorization: ("Bearer ").concat(token), }, responseType: "json"}).then(s => s.body, () => null)) : null
})))
};
} catch (err) {}
}
return {
prova,
aulas: Array.from(document.querySelectorAll("#navbar-content-aluno-pda > ul > li")).reduce<{ title: string; id: string }[]>((acc, e) => {
if (!(e.querySelector("table > tbody > tr > td:nth-child(1) > a"))) return acc;
const a = e.querySelector("table > tbody > tr > td:nth-child(1) > a");
const title = a.getAttribute("title").trim(), pageHref = (new URL(a.getAttribute("href"), index.url)).searchParams.get("ofertaDisciplinaId")
if (pageHref) acc.push({ title, id: pageHref });
return acc;
}, []),
};
}
export type BoletimColaborar = {
detalheBoletimDto: {
detalheDisciplinarDtoList: {
[key: string]: {
acertos: number|null;
apresentacao: boolean;
atividadeAvaliacaoId: number;
atividadeId: number;
atividadeLancamentoId?: number;
atividadeOfertaId: number;
caminhoAtividade1: any;
caminhoAtividade2?: any;
dataAtividade1: string;
dataAtividade2: string;
dataFimGabaritoAluno: string;
dataInicioGabaritoAluno: string;
descricaoAtividade: string;
gabarito: any;
interdisciplinar: boolean;
likes: number;
objetivas?: string;
pontuacao: number;
pontuacaoApresentacao: number;
pontuacaoMinina: number;
pontuacaoTotalApresentacao: number;
sequenciaAvaliacao?: number;
tipoAtividadeId: number;
tipoOfertaId: number;
tipoAtividade: {
enumType: string;
name: string;
};
}[];
};
detalheEngajamentoDtoList: {
descricaoAtividade: string;
pontuacao: number;
tipoAtividade: {
enumType: string;
name: string;
}
}[];
detalheFrequenciaDtoList: {
[type: string]: {
cargaHoraria: number;
cargaHorariaDisciplina: number;
dataAtividade: string;
dataFimAtividade: string;
dataInicioAtividade: string;
dataLancamento: string;
descricaoAtividade: string;
situacao: string;
tipoOfertaId: number;
tipoAtividade: {
enumType: string;
name: string;
};
}[];
}
}
};
export type ActivityType = "Não definido" | "Tele Aula" | "Web Aula" | "Conteúdo Web" | "Referências Digitais" | "Prova Presencial" | "Avaliação Virtual" | "Fórum" | "Portfólio" | "Desafio Nota Máxima" | "Acelere Sua Carreira" | "Avaliação de Proficiência" | "Prova Prática Presencial" | "Atividade Diagnóstica" | "Atividade de Aprendizagem";
export type AtividadeRoot = {
type: Exclude<ActivityType, "Portfólio"|"Prova Presencial">;
descricaoAtividade: string;
atividadeAvaliacaoId: number;
atividadeOfertaId: number;
atividadeId: number;
pontos: {
sequenciaAvaliacao: number;
pontuacaoMinina: number;
pontuacao?: number;
};
raw: BoletimColaborar["detalheBoletimDto"]["detalheDisciplinarDtoList"][string][number];
}|{
type: "Portfólio";
descricaoAtividade: string;
atividadeId: number;
atividadeOfertaId: number;
pontuacaoMinina: number;
pontuacao:number
}|{
type: "Prova Presencial"
descricaoAtividade: string;
atividadeId: number;
atividadeOfertaId: number;
acertos: number;
sequenciaAvaliacao: number;
pontuacaoMinina: number;
pontuacao: number;
};
export type Aula = {
title: string;
date: {
begin: Date;
end: Date;
recovery?: {
begin: Date;
end: Date;
};
};
} & ({
type: "portfólio";
id: string;
points: { hits: number; max: number; };
files: {title: string; url: string;}[];
} | {
type: "teleaula";
id: string;
files: {title: string; url: string;}[];
} | {
type: "avaliação virtual";
id: string;
points: { hits: number; max: number; };
} | {
type: "conteúdo web";
id: string;
} | {
type: "leitura";
files: {title: string; url: string;}[];
externFiles: {title: string; url: string;}[];
} | {
type: "prova presencial da disciplina";
points: {
hits: number;
max: number;
}
}|{
type: string;
[k: string]: any;
});
/**
* Liste as atividade da aula e o boletim.
*
* @param got - got com os cookies
* @param semestre - ID do semetre normalmente nesse formato `{RA}{SEMESTRE}`, exp: {100000}{01},{100000}{02}, {100000}{04}...
* @param id - ID da Aula
*/
export async function Atividades(got: Got, semestre: string, id: string) {
// Mount URL Request's
const dashboard = new URL(path.posix.join("/aluno/timeline/index", semestre), "https://www.colaboraread.com.br"); dashboard.searchParams.set("ofertaDisciplinaId", id);
const botURL = new URL(path.posix.join("/aluno/boletim/getListaDetalheBoletimDto"), "https://colaboraread.com.br/"); botURL.searchParams.set("matriculaId", semestre); botURL.searchParams.set("ofertaDisciplinaId", id); // botURL.searchParams.set("tipoDisciplinaId", "13");
// Request
const index = await got(dashboard), boletimRequest = await got(botURL);
// Parse HTML Page
const doc = new JSDOM(index.body, { url: index.url });
const { window: { document } } = doc;
// Pretty boletim
const boletim = (JSON.parse(boletimRequest.body) as BoletimColaborar[]).map(fn => {
return {
ava: fn.detalheBoletimDto.detalheEngajamentoDtoList.reduce<Record<string, number>>((acc, { descricaoAtividade: name, pontuacao }) => { acc[name] = pontuacao; return acc; }, {}),
frenquencia: Object.keys(fn.detalheBoletimDto.detalheFrequenciaDtoList).reduce<{type: ActivityType; descricaoAtividade: string; situacao?: string; cargaHoraria: { cargaHorariaDisciplina: number; cargaHoraria: number; }; datas: { lancamento: string; atividade: string; inicio: string; fim: string; }; tipo: { id: number; atividade: string; };}[]>((acc, id) => {
const atividade = fn.detalheBoletimDto.detalheFrequenciaDtoList[id];
let ActType: ActivityType = "Não definido";
if (id === "2") ActType = "Tele Aula";
else if (id === "4") ActType = "Web Aula";
else if (id === "17") ActType = "Conteúdo Web";
else if (id === "14") ActType = "Referências Digitais";
else if (id === "13") ActType = "Prova Presencial";
else if (id === "6") ActType = "Avaliação Virtual";
else if (id === "16") ActType = "Fórum";
else if (id === "1") ActType = "Portfólio";
else if (id === "28") ActType = "Desafio Nota Máxima";
else if (id === "31") ActType = "Acelere Sua Carreira";
else if (id === "22") ActType = "Avaliação de Proficiência";
else if (id === "26") ActType = "Prova Prática Presencial";
else if (id === "23") ActType = "Atividade Diagnóstica";
else if (id === "24") ActType = "Atividade de Aprendizagem";
return acc.concat(atividade.map((s): (typeof acc)[number] => ({
type: ActType,
descricaoAtividade: s.descricaoAtividade,
situacao: s.situacao,
cargaHoraria: {
cargaHorariaDisciplina: s.cargaHorariaDisciplina,
cargaHoraria: s.cargaHoraria,
},
datas: {
lancamento: s.dataLancamento,
atividade: s.dataAtividade,
inicio: s.dataInicioAtividade,
fim: s.dataFimAtividade,
},
tipo: {
id: s.tipoOfertaId,
atividade: s.tipoAtividade.name,
}
})));
}, []),
atividades: Object.keys(fn.detalheBoletimDto.detalheDisciplinarDtoList).reduce<AtividadeRoot[]>((acc, id) => {
const atividade = fn.detalheBoletimDto.detalheDisciplinarDtoList[id];
let ActType: Exclude<ActivityType, "Portfólio"|"Prova Presencial"> = "Não definido";
if (id === "1") return acc.concat(atividade.map(({ descricaoAtividade, atividadeId, atividadeOfertaId, pontuacaoMinina, pontuacao }): (typeof acc)[number] => ({ type: "Portfólio", atividadeId, atividadeOfertaId, descricaoAtividade, pontuacaoMinina, pontuacao })));
else if (id === "2") ActType = "Tele Aula";
else if (id === "4") ActType = "Web Aula";
else if (id === "6") ActType = "Avaliação Virtual";
else if (id === "13") return acc.concat(atividade.map(({ descricaoAtividade, atividadeId, atividadeOfertaId, pontuacaoMinina, pontuacao, acertos, sequenciaAvaliacao }): (typeof acc)[number] => ({ type: "Prova Presencial", atividadeId, atividadeOfertaId, descricaoAtividade, acertos, sequenciaAvaliacao, pontuacaoMinina, pontuacao })));
else if (id === "14") ActType = "Referências Digitais";
else if (id === "16") ActType = "Fórum";
else if (id === "17") ActType = "Conteúdo Web";
else if (id === "22") ActType = "Avaliação de Proficiência";
else if (id === "23") ActType = "Atividade Diagnóstica";
else if (id === "24") ActType = "Atividade de Aprendizagem";
else if (id === "26") ActType = "Prova Prática Presencial";
else if (id === "28") ActType = "Desafio Nota Máxima";
else if (id === "31") ActType = "Acelere Sua Carreira";
// Generic
return acc.concat(atividade.map((s): (typeof acc)[number] => {
const { descricaoAtividade, atividadeAvaliacaoId, atividadeOfertaId, atividadeId, sequenciaAvaliacao, pontuacaoMinina, pontuacao } = s;
return {
type: ActType,
descricaoAtividade,
atividadeAvaliacaoId, atividadeOfertaId, atividadeId,
pontos: {
sequenciaAvaliacao,
pontuacaoMinina,
pontuacao,
},
raw: s,
}
}));
}, []),
// atividadesRaw: fn.detalheBoletimDto.detalheDisciplinarDtoList,
};
});
// Extração da aulas
const aulas = Array.from(document.querySelectorAll("#js-activities-container > li")).reduce<Aula[]>((acc, e) => {
if (!(e.querySelector("div:nth-child(2) > div > h4 > span > span"))) return acc;
const typeElement: HTMLSpanElement = e.querySelector("div:nth-child(2) > div > h4 > span > span");
Array.from(typeElement.querySelectorAll("*")).forEach(s => s.remove()); // Remove icon
const type = (typeElement.innerText || typeElement.innerHTML).trim().toLowerCase();
const titleElement: HTMLElement = e.querySelector("div.timeline-panel > div > h4 > small"), title = (titleElement.innerText || titleElement.innerHTML).trim();
if (type === "teleaula") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
const files = Array.from(e.querySelectorAll("div.timeline-panel > div > div.timeline-body > ul > li")).reduce<{title: string, url: string}[]>((acc, a) => {
if (!(a.querySelector("small > a"))) return acc;
const fileA = a.querySelector("small > a");
return acc.concat({
title: fileA.getAttribute("title"),
url: new URL(fileA.getAttribute("href"), index.url).toString(),
});
}, []);
const videoPage = new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url);
return acc.concat({
type: "teleaula",
title,
id: videoPage.searchParams.get("atividadeDisciplinaId"),
date: {
begin: date[0],
end: date[1]
},
files,
});
} else if (type === "portfólio") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-");
let dateS = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-");
if (e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em")) dateS = dateS.concat(e.querySelector("div.timeline-panel > div > p:nth-child(4) > small > em").innerHTML.split("-"));
const dates = dateS.reduce<Date[]>((acc, date, index, l) => {
if (!hour[index]) index = (~index) + l.length;
const [ day, month, year ] = date.trim().split("/"), hor = hour.at(index);
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
const files = Array.from(e.querySelectorAll("div.timeline-panel > div > div.timeline-body > ul > li")).map(e => e.querySelector("small > a")).filter(Boolean).map(s => ({ title: s.getAttribute("title"), url: (new URL(s.getAttribute("href"), index.url)).toString() }))
const portUrl = new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url);
const [ ppo, pmax ] = e.querySelector("div.timeline-panel > div > div:nth-child(6) > div > div > small").textContent.split("de");
return acc.concat({
type,
title,
id: portUrl.searchParams.get("atividadeDisciplinaId"),
date: {
begin: dates[0],
end: dates[1],
...(dates[2] ? { recovery: { begin: dates[2], end: dates[3] } } : {})
},
points: {
hits: parseFloat(ppo.trim()),
max: parseFloat(pmax.trim())
},
files,
});
} else if (type === "avaliação virtual") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
const portUrl = new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url);
const [ ppo, pmax ] = (e.querySelector("div.timeline-panel > div > div:nth-child(5) > div > div > small")||e.querySelector("div.timeline-panel > div > div:nth-child(6) > div > div > small")).textContent.split("de");
return acc.concat({
type, title,
id: portUrl.searchParams.get("atividadeDisciplinaId"),
date: {
begin: date[0],
end: date[1],
},
points: {
hits: parseFloat((ppo||"").trim()),
max: parseFloat((pmax||"").trim())
}
});
} else if (type === "conteúdo web") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
const portUrl = new URL(e.querySelector("div.timeline-panel > div > div.timeline-body > div.row.form-group > div > a").getAttribute("href"), index.url);
return acc.concat({
type, title,
id: portUrl.searchParams.get("atividadeDisciplinaId"),
date: { begin: date[0], end: date[1] },
});
} else if (type === "leitura") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
return acc.concat({
type, title,
date: { begin: date[0], end: date[1] },
files: Array.from(e.querySelectorAll("div.timeline-panel > div > div > ul:nth-child(3) > li")).map(s => s.querySelector("small > a")).filter(Boolean).map(s => ({name: s.getAttribute("title"), url: (new URL(s.getAttribute("href"), index.url)).toString() })),
externFiles: Array.from(e.querySelectorAll("div.timeline-panel > div > div > ul:nth-child(7) > li")||[]).map(s => s.querySelector("small > a")).filter(Boolean).map(s => ({name: s.getAttribute("title")||s.innerHTML.trim(), url: (new URL(s.getAttribute("href"), index.url)).toString() })),
});
} else if (type === "prova presencial da disciplina") {
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
const [ ppo, pmax ] = e.querySelector("div.timeline-panel > div > div:nth-child(4) > div > div > small").textContent.split("de");
return acc.concat({
type, title,
date: { begin: date[0], end: date[1] },
points: {
hits: parseFloat(ppo.trim()),
max: parseFloat(pmax.trim())
}
});
}
const hour = e.querySelector("div.timeline-panel > div > p:nth-child(3) > small > em").innerHTML.trim().split("-"),
date = e.querySelector("div.timeline-panel > div > p:nth-child(2) > small > em").innerHTML.trim().split("-").reduce<Date[]>((acc, date, index) => {
const [ day, month, year ] = date.trim().split("/"), hor = hour[index];
return acc.concat(new Date(String().concat(month, "/", day, "/", year, " ", hor, " GMT-3")));
}, []);
return acc.concat({
type, title, date: { begin: date[0], end: date[1] }
});
}, []);
// Media final calc
let avaPoints = Object.keys(boletim[0].ava).reduce((acc, k) => acc + boletim[0].ava[k], 0);
let avaliacao = boletim[0].atividades.reduce((acc, info) => { if (info.type !== "Avaliação Virtual") return acc; info.raw.sequenciaAvaliacao === 1 && acc.g < info.raw.pontuacao && (acc.g = info.raw.pontuacao); info.raw.sequenciaAvaliacao === 2 && acc.a < info.raw.pontuacao && (acc.a = info.raw.pontuacao); acc.k = acc.g + acc.a; return acc; }, { k: 0, g: 0, a: 0 }).k;
const provas = boletim[0].atividades.reduce((acc, i) => i.type === "Prova Presencial" ? (acc + i.pontuacao) : acc, 0);
if (provas <= 3000) avaPoints = avaliacao = 0;
else if (avaliacao <= 1500) avaPoints = avaliacao = 0;
let Pontos: number[] = [ avaPoints, provas ];
// Retorno
return {
/** Boletim */
boletim,
/** Aulas */
aulas,
/** Media da aula, nota não final */
media: parseFloat(((Pontos.reduce((acc, p) => acc+p, 0) / 100) / 10).toPrecision(2)),
};
}
export async function VideoAulaProgress(got: Got, videoID: string, matriculaId: string, atividadeOfertaId: string) {
const videoDurationURL = new URL("https://colaboraread.com.br/aluno/playerVideo/getVideoDuration");
videoDurationURL.searchParams.set("codigoMidia", videoID);
const videoDuration: number = JSON.parse((await got(videoDurationURL)).body).videoDuration;
return {
url: (new URL(path.posix.join("/video", videoID), "https://mdstrm.com")).toString().concat(".mp4"),
videoDuration,
async updateProgress(currentTime: number) {
if (currentTime > videoDuration) throw new Error("Invalid currentTime!");
await got("https://colaboraread.com.br/aluno/playerVideo/updateProgressoAluno", {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
totalTime: videoDuration,
currentTime,
matriculaId,
atividadeOfertaId
}),
});
}
};
}
export async function saveProgressoEngajamento(got: Got, matriculaId: string, atividadeOfertaId: string, progresso: number) {
await got(new URL("https://colaboraread.com.br/aluno/timeline/saveProgressoEngajamento"), {
method: "post",
headers: { "content-type": "application/json" },
body: JSON.stringify({
matriculaId,
atividadeOfertaId: atividadeOfertaId,
progresso: progresso,
isAluno: true
}),
});
}
export async function syncActivity(got: Got, semestreID: string, atividadeDisciplinaId: string) {
// https://www.colaboraread.com.br/aluno/avaliacao/form/{semestre}?atividadeDisciplinaId={actID}
const actUrl = new URL(path.posix.join("/aluno/avaliacao/form", semestreID), "https://www.colaboraread.com.br");
actUrl.searchParams.set("atividadeDisciplinaId", atividadeDisciplinaId);
return actUrl.toString();
}

@ -1,140 +0,0 @@
import { Context, Telegraf } from "telegraf";
import { Boletim } from "./atividades.js";
import remoteRequest from "./lib/colabora_req.js";
import { decrypt } from "./lib/password.js";
import { User, user } from "./user.js";
const { TELEGRAM_TOKEN = "6705260087:AAHwZBhRPGxle4OuVButQZ5PgsfuonKH4AU" } = process.env;
export interface Middle extends Context {
User?: User;
}
// Start bot
const bot = new Telegraf<Middle>(TELEGRAM_TOKEN);
/*
Telegram markdown:
*bold \*text*
_italic \*text_
__underline__
~strikethrough~
||spoiler||
*bold _italic bold ~italic bold strikethrough ||italic bold strikethrough spoiler||~ __underline italic bold___ bold*
[inline URL](http://www.example.com/)
[inline mention of a user](tg://user?id=123456789)
![👍](tg://emoji?id=5368324170671202286)
`inline fixed-width code`
```block fixed-width code```
```python
pre-formatted fixed-width code block written in the Python programming language```
*/
const helpStartMessage = [
"Olá seja bem-vindo ao colaborar remix!",
"",
"Estou em desenvolvimento por enquanto, então estou limitado aos comandos!",
"",
"/start ou /help: Está mensagem",
"/login: Fazer login no colaborar remix, exemplo: /login <RA/Semestre> <senha>",
"/logout: Fazer logout do bot",
"/list: Lista todas as aulas",
"/boletim <ID>: Pegue o boletim, junto com uma media final",
"/av <ID>: Fazer avaliação do aula pelo telegram (Limitado a algumas avaliações)",
];
bot.start(fn => fn.reply(helpStartMessage.join("\n"))).help(fn => fn.reply(helpStartMessage.join("\n")));
bot.command("login", async (ctx) => {
await ctx.deleteMessage(ctx.message.message_id);
if (await user.findOne({ telegramChats: ctx.message.chat.id })) return ctx.reply("Você já esta logado");
else if (ctx.args.length < 2) return ctx.reply("Login invalido, /login <RA/Semestre> <senha>");
const [ username, pass ] = ctx.args;
const userDb = await user.findOne({ "colaborar.user": username });
if (!userDb) return ctx.reply("Usuario não existe ou não está cadastrado em nosso banco de dados!");
else if (await decrypt(userDb.colaborar.password) !== pass) return ctx.reply("Senha invalida!");
(userDb.telegramChats = (userDb.telegramChats || [])).push(ctx.message.chat.id);
await user.findOneAndUpdate({ _id: userDb._id }, { $set: { telegramChats: userDb.telegramChats } });
ctx.reply(`Olá novamente ${ctx.from.username}`);
return ctx;
});
bot.command("logout", async (ctx) => {
if (await user.findOne({ telegramChats: ctx.message.chat.id })) {
await user.findOneAndUpdate({ telegramChats: ctx.message.chat.id }, { $pull: { telegramChats: ctx.message.chat.id } })
ctx.reply(`🖐️ tchau ${ctx.from.username}`);
return ctx;
}
ctx.reply("Você não está logado");
return ctx;
});
bot.use(async (ctx, next) => {
ctx.User = await user.findOne({ telegramChats: ctx.chat.id });
if (!ctx.User) {
ctx.reply("Faça o login!");
return;
}
next();
});
bot.command("list", async (ctx) => {
for (const semestreId in ctx.User.aulas) {
const message = [ `RA/Semestre: ${semestreId}` ];
for (const aulaId in ctx.User.aulas[semestreId]) message.push("", `ID: ${aulaId}\nName: ${ctx.User.aulas[semestreId][aulaId].name}`);
ctx.reply(message.join("\n"))
}
});
bot.command("boletim", async (ctx) => {
if (ctx.args.length === 0 || ctx.args[0] === "help") return ctx.reply("/boletim <ID da aula>\n\nVocê consegue pegar o ID da aula com o /list");
const semestreId = Object.keys(ctx.User.aulas).find(semestre => Object.keys(ctx.User.aulas[semestre]).some(s => s === ctx.args[0]));
if (!semestreId) return ctx.reply("Aula não está registrada!");
const { got } = await remoteRequest(ctx.User.colaborar.user, ctx.User.colaborar.password);
const bol = await Boletim(got, semestreId, ctx.args[0]);
// Media
await ctx.reply(`Sua media: ${bol.media.nota}`);
if (!bol.media.aprovado) {
const men = [ "Precisa melhorar essa media", "Vamos la ver alguns conteudos, para melhorar essa media sua" ];
await ctx.reply(men.at(men.length * Math.random())||men.at(0));
}
//
const boletim = bol.boletim[0];
await ctx.reply(([ "Nota AVA:" ]).concat(Object.keys(boletim.ava).map(s => String().concat(" ", s, ": ", String(boletim.ava[s])))).join("\n"));
ctx.reply(boletim.atividades.map(s => {
let base = String().concat(s.type, " (", String(s.atividadeId) ,")", ":\n");
if (s.type === "Prova Presencial") {
base = base.concat(
" Acertou: ", String(s.acertos), "\n",
" Ponto: ", String(s.pontuacao), "\n",
" Minimo: ", String(s.pontuacaoMinina)
);
} else if (s.type === "Avaliação Virtual") {
base = base.concat(
" Pontos: ", String(s.pontos.pontuacao),
);
} else if (s.type === "Portfólio") {
base = base.concat(
" Pontos: ", String(s.pontuacao), "\n",
" Minimo: ", String(s.pontuacaoMinina),
);
}
return base;
}).join("\n\n"));
return ctx;
});
bot.command("av", async (ctx) => {
ctx.reply("ainda não funciona, estamos trabalhando nessa parte ainda!");
});
// Start
bot.launch();
console.log("Telegram bot started");
process.once("SIGINT", () => bot.stop("SIGINT"))
process.once("SIGTERM", () => bot.stop("SIGTERM"))

@ -1,53 +0,0 @@
import { insertActivitysToCollection } from "./atividades.js";
import colab from "./lib/colabora_req.js";
import { database } from "./lib/db_connect.js";
import { Aulas, Cursos } from "./lib/sync.js";
export type User = {
/** User ID */
userID: string;
/** Cookie token */
sessions: string[];
/** Telegram chat id */
telegramChats: number[];
/** Ananhanguera user auth */
colaborar: {
user: string;
password: string;
};
/** Semestre -> Matricula -> Aulas[] */
aulas: Record<string, Record<string, { name: string; ids: string[] }>>;
};
export const user = database.collection<User>("User");
export async function syncUser(userID: string, semestre?: string) {
const userData = await user.findOne({ userID });
if (!userData) throw new Error("");
const { got } = await colab(userData.colaborar.user, userData.colaborar.password);
const cursos = await Cursos(got)
if (typeof semestre === "string" && semestre.length > 0) {
userData.aulas[semestre] = {};
for (const aula of (await Aulas(got, semestre)).aulas) {
const ids = await insertActivitysToCollection(got, semestre, aula.id);
userData.aulas[semestre][aula.id] = { name: aula.title, ids };
}
} else {
for (const curso of cursos) {
for (const { semestreID } of curso.semestres) {
userData.aulas[semestreID] = {};
for (const aula of (await Aulas(got, semestreID)).aulas) {
const ids = await insertActivitysToCollection(got, semestreID, aula.id);
userData.aulas[semestreID][aula.id] = { name: aula.title, ids };
}
}
}
}
await user.findOneAndReplace({ _id: userData._id }, userData);
return userData;
}