Skip to content

Instantly share code, notes, and snippets.

@saulodias
Last active July 2, 2021 01:28
Show Gist options
  • Save saulodias/5589b8b060331ce2e2dcaa4151cd88ee to your computer and use it in GitHub Desktop.
Save saulodias/5589b8b060331ce2e2dcaa4151cd88ee to your computer and use it in GitHub Desktop.
Adiciona suporte para o Google Calendar na grade de horários do CEFET/RJ.
// ==UserScript==
// @name CEFET Google Calendar
// @namespace https://gist.github.com/
// @version 0.3
// @description Adiciona suporte para o Google Calendar na grade de horários do CEFET/RJ.
// @author https://github.com/saulodias
// @match https://alunos.cefet-rj.br/aluno/aluno/quadrohorario/*
// @icon https://upload.wikimedia.org/wikipedia/commons/a/a5/Google_Calendar_icon_%282020%29.svg
// @grant none
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
const querySelectorAllAsync = (el) => {
return new Promise((resolve) => {
let element;
const check = (el) => {
element = document.querySelectorAll(el);
if (element.length) {
resolve(element);
} else {
setTimeout(() => {
check(el);
}, 100);
}
};
check(el);
});
};
const trim = (value) => value.replace(/\s+/g, " ").trim();
const mapRow = (headings) => {
return ({ cells }) => {
return [...cells].reduce(function (result, cell, i) {
const input = cell.querySelector("input,select");
let value;
if (input) {
value = input.type === "checkbox" ? input.checked : input.value;
} else {
value = cell.innerText;
}
value = trim(value);
return Object.assign(result, { [headings[i]]: value });
}, {});
};
};
/**
* Parse table by Wicky Nilliams
* https://gist.github.com/WickyNilliams/9252235
* License: MIT
*/
const parseTable = (table) => {
var headings = [...table.tHead.rows[0].cells].map(
(heading) => heading.innerText
);
return [...table.tBodies[0].rows].map(mapRow(headings));
};
const getDadosTurma = (link) => {
return new Promise((resolve) => {
$("<div />").load(link, (data) => {
const doc = new DOMParser().parseFromString(data, "text/html");
const tableVagas = doc.getElementsByClassName("tablevagas")[0];
tableVagas.parentNode.removeChild(tableVagas); // remove tablevagas
const turma = trim(doc.querySelectorAll(".topopage")[0].innerText).split(
" "
)[3];
const labels = doc.querySelectorAll(".label"); // querySelector gets a static collection
const dadosGerais = { turma };
for (let l of labels) {
const labelParent = l.parentNode;
const label = l.parentNode.removeChild(l);
dadosGerais[String(trim(label.innerText).replace(":", ""))] = trim(
labelParent.innerText
);
}
const docentesTabela = doc
.querySelectorAll('[title="Docentes"]')[0]
?.querySelectorAll("table")?.[0];
const docentes = docentesTabela && parseTable(docentesTabela);
const horariosTabela = doc
.querySelectorAll("[title=Horários]")[0]
?.querySelectorAll("table")?.[0];
const horarios = horariosTabela && parseTable(horariosTabela);
const espacoFisicoTabela = doc
.querySelectorAll('[title="Espaço Físico"]')[0]
?.querySelectorAll("table")?.[0];
// Caso especial quando a tabela de locais vem com informação pra duas linhas mas a segunda linha vem zoada
const espacoFisicoCelulas = Array.from(
espacoFisicoTabela.getElementsByTagName("td")
);
if (espacoFisicoCelulas.length === 7) {
const segundaLinhaCelulas = espacoFisicoCelulas.splice(4);
segundaLinhaCelulas.unshift(document.createElement("td")); // adiciona célula vazia no inicio da segunda linha
let primeiraLinhaHTML = "";
espacoFisicoCelulas.forEach((cel) => {
primeiraLinhaHTML += cel.outerHTML;
});
let segundaLinhaHTML = "";
segundaLinhaCelulas.forEach((cel) => {
segundaLinhaHTML += cel.outerHTML;
});
const tbodyHTML =
"<tr>" + primeiraLinhaHTML + "</tr><tr>" + segundaLinhaHTML + "</tr>";
espacoFisicoTabela.getElementsByTagName("tbody")[0].innerHTML =
tbodyHTML;
} // fim do caso especial
const espacoFisico = espacoFisicoTabela && parseTable(espacoFisicoTabela);
resolve({ dadosGerais, docentes, horarios, espacoFisico });
});
});
};
const normalizarNome = (texto) => {
const excecoes = ["a", "e", "o", "da", "de", "do", "das", "dos"];
const preservar = ["I", "II", "III", "IV", "V"];
return texto
.split(" ")
.map((p) => (preservar.includes(p) ? p : p.toLowerCase()))
.map((p) => (excecoes.includes(p) ? p : p[0].toUpperCase() + p.slice(1)))
.join(" ");
};
const toRFC5545UTC = (date = new Date()) =>
new Date(date).toISOString().split(".")[0].replace(/[-:]/g, "") + "Z";
const toLocalTimezone = (date = new Date()) =>
new Date(date)
.toLocaleString("sv-SE", { hour12: false })
.replace(" ", "T")
.replace(/[-:]/g, "");
const nextWeekdayDate = (day_in_week, date = null) => {
const ret = new Date(date || new Date());
ret.setDate(ret.getDate() + ((day_in_week - 1 - ret.getDay() + 7) % 7) + 1);
return ret;
};
const proximoDia = (hhmm, dia, data) => {
const [hh, mm] = hhmm.split(":").map((v) => Number(v));
const d = data || new Date();
d.setHours(hh);
d.setMinutes(mm);
return nextWeekdayDate(dia, d);
};
const data = (ddmmyyyy) => {
const [dd, mm, yyyy] = ddmmyyyy.split("/");
return new Date(yyyy, mm - 1, dd);
};
/**
* https://dylanbeattie.net/2021/01/12/adding-events-to-google-calendar-via-a-link.html
*/
const gerarEventoGoogleCalendar = (dados) => {
const { titulo, detalhes, local, horaInicio, horaFim, dataFim } = dados;
const apiUrl = "https://calendar.google.com/calendar/u/0/r/eventedit";
const text = titulo;
const dates = toRFC5545UTC(horaInicio) + "/" + toRFC5545UTC(horaFim);
const details = detalhes;
const location = local;
const recur = "RRULE:FREQ=WEEKLY;UNTIL=" + toRFC5545UTC(dataFim);
const paramsString = new URLSearchParams({
text,
dates,
details,
location,
recur,
}).toString();
return apiUrl + "?" + paramsString;
};
const templateVCalendar = (vEvents) =>
"BEGIN:VCALENDAR\r\n" +
"PRODID:-//Google Inc//Google Calendar 70.9054//EN\r\n" +
"VERSION:2.0\r\n" +
"CALSCALE:GREGORIAN\r\n" +
"METHOD:PUBLISH\r\n" +
"X-WR-CALNAME:CEFET\r\n" +
"X-WR-TIMEZONE:America/Sao_Paulo\r\n" +
"BEGIN:VTIMEZONE\r\n" +
"TZID:America/Sao_Paulo\r\n" +
"X-LIC-LOCATION:America/Sao_Paulo\r\n" +
"BEGIN:STANDARD\r\n" +
"TZOFFSETFROM:-0300\r\n" +
"TZOFFSETTO:-0300\r\n" +
"TZNAME:-03\r\n" +
"DTSTART:19700101T000000\r\n" +
"END:STANDARD\r\n" +
"END:VTIMEZONE\r\n" +
vEvents +
"END:VCALENDAR";
const gerarVEvent = (dados) => {
const { titulo, detalhes, local, horaInicio, horaFim, dataFim } = dados;
return (
Object.entries({
BEGIN: "VEVENT",
"DTSTART;TZID=America/Sao_Paulo": toLocalTimezone(horaInicio),
"DTEND;TZID=America/Sao_Paulo": toLocalTimezone(horaFim),
RRULE: "FREQ=WEEKLY;UNTIL=" + toRFC5545UTC(dataFim),
DTSTAMP: toRFC5545UTC(),
DESCRIPTION: detalhes,
LOCATION: local,
SEQUENCE: "0",
STATUS: "CONFIRMED",
SUMMARY: titulo,
TRANSP: "OPAQUE",
END: "VEVENT",
})
.map(([key, value]) => `${key}:${value}`)
.join("\r\n") + "\r\n"
);
};
const gerarResumo = (dados) => {
const { detalhes, local } = dados;
return `&#10;${detalhes}&#10;${local}`;
};
const gerarDadosDaAula = (cadeira, dia, hora) => {
const aula = cadeira.horarios.findIndex(
(h) => h["Dia da Semana"][0] - 1 === dia && h["Hora Início"] === hora
);
const turma = cadeira.dadosGerais.turma;
const titulo = normalizarNome(cadeira.dadosGerais.Disciplina);
const horario = cadeira.horarios[aula] || cadeira.horarios[0];
const docente = cadeira.docentes?.[aula] || cadeira.docentes?.[0];
const nome = docente?.["Nome do Docente"];
const prof = (nome && ` com prof(a). ${normalizarNome(nome)} `) || "";
const detalhes = `Aula ${horario.Aula.toLowerCase()} ${prof}- Turma: ${turma}`;
const espaco = cadeira.espacoFisico[aula] || cadeira.espacoFisico[0];
const local = [
espaco["Nome do Campus"],
espaco["Nome do Prédio"],
espaco["Número da Sala"],
]
.filter((v) => !!v)
.join(", ");
const dataInicio = data(horario["Data Início Período"]);
const dataFim = data(horario["Data Fim Período"]);
const horaInicio = proximoDia(
horario["Hora Início"],
horario["Dia da Semana"][0] - 1,
dataInicio
);
const horaFim = proximoDia(
horario["Hora Fim"],
horario["Dia da Semana"][0] - 1,
dataInicio
);
return { titulo, detalhes, local, horaInicio, horaFim, dataFim, turma };
};
(async () => {
"use strict";
await querySelectorAllAsync("#quadrohorario");
const aulas = document.getElementsByClassName("turmaqh");
const periodos = [];
let vEvents = "";
for (let aula of aulas) {
const a = aula.getElementsByTagName("a")[0];
const celula = aula.parentNode.parentNode;
const dia = celula.cellIndex - 1;
const periodo = a.title + dia;
const hora = a.title.split("Horário: ")[1].split(" às ")[0];
if (periodos.includes(periodo)) continue;
const link = a.href;
const dadosTurma = await getDadosTurma(link);
const dados = gerarDadosDaAula(dadosTurma, dia, hora);
const botaoGCalendar = document.createElement("a");
const linkGoogleCalendar = gerarEventoGoogleCalendar(dados);
botaoGCalendar.href = linkGoogleCalendar;
botaoGCalendar.target = "_blank";
botaoGCalendar.innerHTML = `<img src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Google_Calendar_icon_%282020%29.svg"
alt="Adicionar ao calendário do Google"
title="Adicionar ao calendário do Google ${gerarResumo(dados)}"
height="30"
width="30" />`;
celula.appendChild(botaoGCalendar);
vEvents += gerarVEvent(dados);
periodos.push(periodo);
}
const vCalendar = templateVCalendar(vEvents);
const url = window.URL.createObjectURL(
new Blob([vCalendar], { type: "text/plain;charset=utf-8" })
);
const botaoIcs = document.createElement("a");
botaoIcs.href = url;
botaoIcs.download = "cefet_horarios.ics";
const aHTML = `<img src ="https://upload.wikimedia.org/wikipedia/commons/c/cd/Circle-icons-calendar.svg"
width="49"
height="49"
alt = "Exportar como ICS"
title = "Exportar como ICS (Google Calendar, Outlook Calendar e Apple Calendar)"/>`;
botaoIcs.innerHTML = aHTML;
const topoHorarios = document.getElementsByClassName("topohorarios")[0];
topoHorarios.style.display = "flex";
topoHorarios.appendChild(botaoIcs);
})();

Grade horária CEFET Google Calendar

Este script adiciona botões na grade horária do CEFET/RJ para exportar as aulas para o Google Calendar. Também é possível exportar em massa clicando no botão no canto superior direito do calendário. O arquivo salvo é aceito pelo Google Calendar, Outlook Calendar, Apple Calendar, e outras aplicações de calendário que suportam o formato ICS.

Como utilizar

Instalação do Tampermonkey (Extensão do Google Chrome)

Clique aqui para ir para a página da extensão, e depois clique no botão Usar no Chrome. O Tampermoney é um gerenciador de userscript para o Google Chrome. Ele permite que scripts de terceiros sejam utilizados para adicionar funcionalidade a páginas da web, como botões de atalho, customização de elementos e etc.

Instalação do Script

Na página do script (cefet-google-calendar.user.js) no Github, ao lado do nome do script clique no botão que diz Raw para visualizar o código puro do script.

Se você tiver instalado o Tampermonkey com sucesso, você verá um botão Instalar na página. Clique nele para instalar o script no seu browser.

Pronto!

Agora você já pode abrir a página da grade horária do CEFET no Portal do Aluno e visualizar tanto os botões para exportar aulas para o Google Calendar Logo do Google Calendar, quanto o botão no canto superior direito da página Botão Exportar para ICS, para exportar em massa para o Google Calendar, Outlook Calendar, Apple Calendar e outros aplicativos que suportam o formato ICS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment