您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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,R0lGODlhMgAoAPcAAF5eXlfQRuP04WFhYWpqapKSkvHx8VbhONToxsnRw6mpqWPWOYKCgs7dv56enlziR76+vubm5qPYh42NjYK8aXZ2dsHBwdnl0pWVlWjCQzy+KHT6Od3d3WvqSK2trVHBK87OzkpKSm68UfX19bTdplXNNNfX12bzK22VduDg4Pz8/ErTNdnZ2bPooXJycvH77Hp6ejbCGt7e3mjnK/7+/vr6+pbZckCxQ+Pj4m1tbdra2oWFhVroKOzs7KfHmlPPYtTU1GVlZcbkumHhPKfVlIH4NLCwsFVVVVTSKmTMWOnp6YnKfVpaWlTfLkjFN+Xu3kBAQPj4+GTPFrq6usjIyLa2tpiYmLjmlerq6nyQhoiIiKenp9HR0aKiori4uGyZWn1+fnHwQJK3hIflR0O6PKCgoHizY0rJF5qammbKP0zrNMbGxmrmHpGki1FRUdDYzMrKymd0aaSkpNDQ0MTExH7VYvn++dr5u7Ozs1vVNILbUVrWJdXc1K2xp7Kysu7v7lzbI/z++5TtUkTXJWbbJGDWK1TZMUVFRVvKK8POumLdMljSHUjZMGTpPVndHUXUKmbeRIuLi1jaN5WekljTOaOkof7//ljMB6TydlymT12/PGT5P2r1OHSsd/H+5JrkdXX4TlPTGMvLy5PyWamuqKywrYPIaV/uOIfVZdnw3XvzSfPz82ruMZCQkErXHFHMF1tkWZCcjP39/VOgW7P3ilLWJlXaJ13ZLc/tr29/ds/6sOTj5KGxkL7BuXLxVdze3IyrgIS0jJq3larhlff+8b34nXbjLUy/T/v+91u6T1nQPL/Tt9re2LDcguvr51zdO47sUJzbZj2uK06vOpucm+f4zn3ZSU7oIWnhO2/+Surp66etpbGzsGH3NHzsO1z8KWvUXm/xN2/8MX7tXUjeJ22yZGDNLX2ob2DXIj+5W8D0nWfdLev07U7XJk/XKsfox43oZ9XvyZztZJPwYWXXS2LYSmfQRWvTSWrbRm7dSGWthlLfTnLyTU/PLHj3RP///yH5BAAAAAAALAAAAAAyACgAAAj/AP8JHEiwoMGDCBMqXMiw4cAXT2Q5nEgR2R0bCCxR3LjQkzprLWhMpKFCBY2TKFOKbGip2rxGJAI1lFXDQA8dJjiwYKGDQwocEXr0+GPAwAgVslYWDHQHGrYWdhjSWMVhigMFVTC4yOECBoMdrax0UWAEAhVRQHQYkEgwkK4iMyREXagixZYCMIIwMOIgSA6vO7QUsCLHQxUIQHCkMEGFQxS2xIptOFHnhVQTOxx0cdHFgwcFkbbgqdKFAYMJGLpMMTIlwp8eOERRSVGDGC1x3cAJaNgDTRdR1BhMsTKhSqsyVbxggAFGS6QCChRsWaMEi1AgEBpg2sApSaqGNSAU/wCyhloBDxi0TKFWoT3XCmAYaOmypYsRDhG0/flzYdQGf/YIoRFdOrSCBxcWGIHHZzsoMMUAIbhxBAAE5FABDFpgoKECflgAwhtX+OPPPT6swpAsSlRBDWsWWABEggREAgQDEh4x4QAuyNdKATxOIJYE44CSjxilmBCFCgnR0AMQc1AxxRQQGGGEHFqEUAEQU2ihBWGGeahDChEoYUAUI+BSRDYdLPEEDhzggMVRSg2kgg5AcCGDDCbMAcdqDjCABhc6iLIGFVzkxMGhOCghFBbxCLLBNwFcIFQEKfwkZg0FqWDCppVyAcdPlbIAxKZAUFGFAl1SsQYdVIAARAOCqP/iiyNE/PJLpRGEmWsPEWAq0B+zfZmCqBz0VKxOpIoyhRydVdHik4nIEw4/+JjCx6Y9WdrDKjhwEUEUAinRhQVnzWEoqBwwIwQJb7BggigQlKEABFN81kc0J2zSiAhvYLuTsHTSsakSAhmQoxdekAtCoTKk8MYwHfBgwxscmICFKCz8gYcCvTTjTTff9CPMFp55gIdZ62lRgQMuRoCkCmAc0YUcrEGwBhxcNADPKUMsYIsNv/TkzAj/bFHJFWF0cMo1ZmAwIQAADMAANVowEQIUE7iYAhYnKTAAj2KRhYcXPmDDgyQLGFILCTIAAaYSRvAyBiuNDPFBG1scAcXeUBz/UqGEBGyxaZs10FDFDhVoGZZmW5BCgSK19EMJJfbEY4A2uwCRyCfGsDFDAOdQUwUaFeSQAwEDUBiJEXSAYIIMiUZBwxpWwMdAcwVQ48AWbaQByCMlKNOOHn+s8gcOVzQyAxsBJJOAbKvSSy81qVGDBxBAsIAf0SmQTsAEMHilRSvUtPJFIYwgUQI5rpDAgQwNjKEGD8ocQwE3o3kBgc2bBQGA1RXAGRDeBy4DsCcEOZADBm4Hhh3sIA4ZMMQikMCIR9QhASBohiLUsI9jiGASaNAMqvxQBRiEwAUN7AoVsLcGCKyCBhGoGhTcYAUHWMEKrYiECwZwjjzsoRaMWEEa/4iwDD0cYB8/IAMwrECNmclhCx1iwBFc4IIgHIEAVQDBBMCgAHD1wAqH6NsRglCB01BjK7FIgyQMYQskmEMPqIDEKZJYjkhMYDBl2EJ90ECAQ/hxb4eIxA6+RwckKckLDADAEZgwgCAQoAIACEIBKFCPAziiEHtwxAMe0AQnkCEXNVqkCxxQgCOEIIx8C0FqjEAFGbDlHyrAgQdcwARGDmAAR2hkLJJwgHUQAhBN2CQSYtAJN/CNbzmwAgAixMgKTEAOEOBCCgxQuIKMAARWIAATouaGHFTBCEtYgS8VsclnnEEaWajlNisEgwl0gZQYuGEBwNcFE/QgTgRRgQGoMPwB/11RAQxogznQcYt60AOTMcgEGNrjlUi0ohUT0AID2lkB/9WSABhSQATwKacIlNBqMDDCNkSwCHcEIA1SCEUMUEAARzqSAKi75QAI0IodyjR1R8gBCDg6EBrUwARdyEEItOAFYOxhECtAhBTOkI44bDNqN5WpI28KNQptoQdSqYEOnBYCWGiCHI94xSU0oI+qRvWWVU3rTBvEBdk5hAYjEEUrcvAFSTQhFGe4QRbSatZGOvI9DMCAAtawi2pypAZ/4IMIIDGEEsyiFGVAQzypUYYnmgwCdEALfgyAJI4cZBnTcII0gtFZz5q2pwKIxzvYcdrWuva1sF1IQAAAOw==" />' : ''} </td> `; }).join('\n')} </tr>`; }).join('\n')} </table> `; settingPopup.innerHTML = table; } const main = async () => { insertStartButton(); insertSettingButton(); insertSettingPopup(); } (function() { 'use strict'; main(); })();