Greasy Fork is available in English.
Auto book the time slot in CDC
// ==UserScript==
// @name CDC Booking Script
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Auto book the time slot in CDC
// @author Liu Chao
// @include https://www.cdc.com.sg:8080/NewPortal/Booking/BookingPL.aspx
// @icon https://www.google.com/s2/favicons?domain=undefined.
// @grant none
// @license MIT
// ==/UserScript==
/* jshint esversion: 8 */
const isTesting = false;
// settings
// const preferredTimeSlots = [
// {
// day: 'WED',
// slot: '16:25 - 18:05'
// }
// ]
const getPreferredTimeSlots = () => {
return JSON.parse(localStorage.getItem('preferredTimeSlots') || '[]');
}
const isLoading = () => {
const loadingSpinner = document.querySelector('#ctl00_ContentPlaceHolder1_UpdateProgress1');
return loadingSpinner && loadingSpinner.style.display !== 'none';
}
const toggleSelector = async () => {
const s = document.querySelector('#ctl00_ContentPlaceHolder1_ddlCourse')
const toBeSelected = s.selectedIndex === 0 ? 1 : 0;
s.selectedIndex = toBeSelected;
s.onchange()
await sleep(1000);
while(isLoading()) {
await sleep(1000);
}
return Promise.resolve(toBeSelected);
}
const isTimeSlotPreferred = ({date= '', day ='', slot = ''}) => {
if (!date) {
return false;
}
const preferredTimeSlots = getPreferredTimeSlots();
const matched = preferredTimeSlots.find((timeSlot) => timeSlot.day === day && timeSlot.slot === slot);
return !!matched;
}
const getAvailableSlots = () => {
const table = document.querySelector('#ctl00_ContentPlaceHolder1_gvLatestav');
if (!table) {
return [];
}
const rows = table.querySelectorAll('tr');
const timeSlotRow = rows[0];
let allSlots = [];
rows.forEach((row, index) => {
if (index !== 0) {
const slots = []
const cells = row.querySelectorAll('td');
const timeSlotCells = timeSlotRow.querySelectorAll('th');
cells.forEach((cell, index) => {
const slotInput = cell.querySelector('input');
if (slotInput) {
const src = slotInput.src || '';
if (src.includes('Images1')) {
const date = cells[0].textContent;
const day = cells[1].textContent;
const slot = timeSlotCells[index].textContent.slice(1)
slots.push({
slotInput, date, day, slot
})
}
}
});
allSlots = allSlots.concat(slots);
}
});
return allSlots;
}
const reserve = (availableSlots) => {
const timeAvailableSlot = availableSlots.find((slot) => isTimeSlotPreferred(slot));
if (timeAvailableSlot) {
const slotInput = timeAvailableSlot.slotInput;
if (slotInput) {
slotInput.click();
return timeAvailableSlot;
}
}
return false;
}
const run = async () => {
try {
let selectedIndex;
if (isTesting) {
selectedIndex = 1;
} else {
selectedIndex = await toggleSelector();
}
if (selectedIndex === 0) {
await toggleSelector();
}
const availableSlots = getAvailableSlots();
const hasResult = !!availableSlots.length;
if (hasResult) {
const reservedSlot = reserve(availableSlots);
let msg;
if (reservedSlot) {
msg = `Slot is reserved\n${reservedSlot.date} ${reservedSlot.day} ${reservedSlot.slot}`;
} else {
const slotsMsg = `${availableSlots.map(({date, day, slot}) => `${date} ${day} ${slot}`).join('\n')}`;
msg = `Slots are available\n${slotsMsg}`;
}
sendMessage(`[${new Date().toLocaleString('en-ca')}] ${msg}`);
}
console.log(`Checked again with result: ${hasResult}`);
return Promise.resolve(hasResult);
} catch(e) {
return Promise.reject(`error: ${e}`)
};
}
const getRandomTime = (time) => {
const seed = Math.random();
return parseInt(time * (0.5 + seed));
}
const sleep = (interval, random=false) => {
return new Promise((resolve) => {
window.setTimeout(resolve, random ? getRandomTime(interval) : interval);
});
}
// https://simpleguics2pygame.readthedocs.io/en/latest/_static/links/snd_links.html
const audio = new Audio('http://codeskulptor-demos.commondatastorage.googleapis.com/descent/gotitem.mp3');
const sendMessage = (msg) => {
// alert(msg);
console.log(msg);
audio.play();
}
let startButton;
let settingButton;
let settingPopup;
const start = async () => {
const hasResult = await run() ;
if (!isTesting) {
if (hasResult) {
await sleep(3 * 1000, true);
} else {
await sleep(10 * 1000, true);
}
start();
}
}
const insertStartButton = () => {
startButton = document.createElement('button');
startButton.className = 'start-button';
startButton.textContent = 'start';
const startButtonStyle = document.createElement('style');
startButtonStyle.innerHTML = `
.start-button {
position: fixed;
bottom: 70px;
right: 50px;
width: 100px;
padding: 5px;
line-height: 1;
background-color: #ff9933;
border: none;
border-radius: 10px;
font-size: 18px;
cursor: pointer;
}
`;
document.head.appendChild(startButtonStyle);
document.body.appendChild(startButton);
let hasStarted = false;
startButton.addEventListener('click', async () => {
try {
if (!hasStarted) {
start();
hasStarted = true;
startButton.textContent = 'running...'
}
} catch(e) {
startButton.textContent = 'error'
console.log('error', e);
}
});
}
const insertSettingButton = () => {
settingButton = document.createElement('button');
settingButton.className = 'setting-button';
settingButton.textContent = 'setting';
const settingButtonStyle = document.createElement('style');
settingButtonStyle.innerHTML = `
.setting-button {
position: fixed;
bottom: 30px;
right: 50px;
width: 100px;
padding: 5px;
line-height: 1;
background-color: #ff9933;
border: none;
border-radius: 10px;
font-size: 18px;
cursor: pointer;
}
`;
document.head.appendChild(settingButtonStyle);
document.body.appendChild(settingButton);
settingButton.addEventListener('click', async () => {
toggleSettingPopup();
});
}
const toggleSettingPopup = () => {
if (window.getComputedStyle(settingPopup).display === 'none') {
settingPopup.style.display = 'initial';
} else {
settingPopup.style.display = 'none';
}
}
const insertSettingPopup = () => {
settingPopup = document.createElement('div');
settingPopup.className = 'setting-popup';
const settingPopupStyle = document.createElement('style');
settingPopupStyle.innerHTML = `
.setting-popup {
display: none;
position: fixed;
bottom: 65px;
right: 50px;
width: 900px;
padding: 10px;
background-color: #ff9933;
border: none;
border-radius: 10px;
font-size: 22px;
}
.setting-popup label {
margin-bottom: 10px;
display: block;
}
.setting-popup table {
width: 100%;
table-layout: auto;
border-collapse: collapse;
}
.setting-popup table th,
.setting-popup table td {
text-align: center;
border: 1px solid black;
}
.setting-popup table th {
padding: 5px;
}
.setting-popup table td {
height: 35px;
}
.setting-popup table img {
width: 30px;
}
.setting-popup table .selectable {
cursor: pointer;
}
.setting-buttons {
display: flex;
flex-wrap: wrap;
}
.setting-buttons .toggle-button {
border: none;
padding: 5px;
border-radius: 5px;
margin-right: 5px;
margin-bottom: 5px;
cursor: pointer;
}
.setting-buttons .toggle-button.active {
background-color: red;
}
`;
document.head.appendChild(settingPopupStyle);
document.body.appendChild(settingPopup);
renderSettingTable();
}
const toggleTimeSlot = (day, slot) => {
const savedPreferredTimeSlots = getPreferredTimeSlots()
const matched = savedPreferredTimeSlots.find((timeSlot) => timeSlot.day === day && timeSlot.slot === slot);
if (matched) {
savedPreferredTimeSlots.splice(savedPreferredTimeSlots.indexOf(matched), 1);
} else {
savedPreferredTimeSlots.push({
day,
slot
})
}
localStorage.setItem('preferredTimeSlots', JSON.stringify(savedPreferredTimeSlots))
renderSettingTable();
}
const renderSettingTable = () => {
const slots = ['08:30 - 10:10', '10:20 - 12:00', '12:45 - 14:25', '14:35 - 16:15', '16:25 - 18:05', '18:50 - 20:30', '20:40 - 22:20'];
const days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
window.toggleTimeSlot= toggleTimeSlot;
const savedPreferredTimeSlots = getPreferredTimeSlots();
const table = `
<label>Preferred Time Slots</label>
<table>
<thead>
<tr>
<th>Day</th>
${slots.map(slot => `<th>${slot}</th>`).join('\n')}
</tr>
</thead>
${days.map(day => {
return `
<tr>
<td>${day}</td>
${slots.map((slot) => {
const isSelected = !!savedPreferredTimeSlots.find((timeSlot) => timeSlot.day === day && timeSlot.slot === slot);
return `
<td class="selectable" onclick="toggleTimeSlot('${day}', '${slot}', '${isSelected}')">
${isSelected ? '<img src="data:image/gif;base64,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" />' : ''}
</td>
`;
}).join('\n')}
</tr>`;
}).join('\n')}
</table>
`;
settingPopup.innerHTML = table;
}
const main = async () => {
insertStartButton();
insertSettingButton();
insertSettingPopup();
}
(function() {
'use strict';
main();
})();