CDC Booking Script

Auto book the time slot in CDC

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
})();