Last active
November 3, 2023 00:33
-
-
Save thevtm/042e4e7289c86c6eccd01d19117da191 to your computer and use it in GitHub Desktop.
Berlin Appointments Alert
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Constants | |
const APPOINTMENTS_CALENDAR_URL = "/terminvereinbarung/termin/all/120686/" | |
const WAIT_DURATION_MS = 3 * 60 * 1000 | |
const INITIAL_STATE = "INIT_STATE" | |
const SETTING_UP_STATE = "SETTING_UP_STATE" | |
const CHECKING_APPOINTMENTS_STATE = "CHECKING_APPOINTMENTS_STATE" | |
const WAITING_STATE = "WAITING_STATE" | |
const ALARM_STATE = "ALARM_STATE" | |
const UNKNOWN_STATE = "UNKNOWN_STATE" | |
const SET_UP_EVENT_TYPE = "SET_UP_EVENT" | |
const START_EVENT_TYPE = "START_EVENT" | |
const WAIT_TIMER_END_EVENT_TYPE = "WAIT_TIMER_END_EVENT" | |
const IFRAME_ON_LOAD_EVENT_TYPE = "IFRAME_ON_LOAD_EVENT" | |
const APPOINTMENTS_FOUND_EVENT_TYPE = "APPOINTMENTS_FOUND_EVENT" | |
const NO_AVAILABLE_APPOINTMENTS_EVENT_TYPE = "NO_AVAILABLE_APPOINTMENTS_EVENT" | |
const APPOINTMENTS_ERROR_EVENT_TYPE = "APPOINTMENTS_ERROR_EVENT" | |
const APPOINTMENTS_AVAILABLE_PAGE = "APPOINTMENTS_AVAILABLE_PAGE" | |
const APPOINTMENTS_UNAVAILABLE_PAGE = "APPOINTMENTS_UNAVAILABLE_PAGE" | |
const APPOINTMENTS_ERROR_PAGE = "APPOINTMENTS_ERROR_PAGE" | |
const APPOINTMENTS_UNKNOWN_PAGE = "APPOINTMENTS_UNKNOWN_PAGE" | |
// Calculations | |
function detectPage(document) { | |
const title_el = document.querySelector('h1') | |
if (title_el == null) { | |
return APPOINTMENTS_UNKNOWN_PAGE | |
} | |
const title = title_el.innerText | |
if (title === "Terminvereinbarung") { | |
return APPOINTMENTS_AVAILABLE_PAGE | |
} | |
if (title === "Leider sind aktuell keine Termine für ihre Auswahl verfügbar.") { | |
return APPOINTMENTS_UNAVAILABLE_PAGE | |
} | |
if (title === "Termin buchen") { | |
return APPOINTMENTS_ERROR_PAGE | |
} | |
if (title === "404 - Unbekannte Adresse") { | |
return APPOINTMENTS_ERROR_PAGE | |
} | |
return APPOINTMENTS_UNKNOWN_PAGE | |
} | |
function parseCalendarAvailableDates(calendar_el) { | |
const MONTH_NAME_TO_NUMBER_DE_MAP = { | |
"Januar": 0, | |
"Februar": 1, | |
"März": 2, | |
"April": 3, | |
"Mai": 4, | |
"Juni": 5, | |
"Juli": 6, | |
"August": 7, | |
"September": 8, | |
"Oktober": 9, | |
"November": 10, | |
"Dezember": 11, | |
} | |
const month_year_el = calendar_el.querySelector(".month") // innerText == "Oktober 2023" | |
const [month_text, year_text] = month_year_el.innerText.split(" ") | |
const month_number = MONTH_NAME_TO_NUMBER_DE_MAP[month_text] | |
const year_number = parseInt(year_text) | |
const available_days_number = Array.from(calendar_el.querySelectorAll("tbody a")).map(el => parseInt(el.innerText)) | |
return available_days_number.map(d => new Date(year_number, month_number, d)) | |
} | |
function parseCalendars(document) { | |
const first_calendar_el = document.querySelector(".calendar-month-table:nth-child(1)") | |
const second_calendar_el = document.querySelector(".calendar-month-table:nth-child(2)") | |
return [...parseCalendarAvailableDates(first_calendar_el), | |
...parseCalendarAvailableDates(second_calendar_el)] | |
} | |
// Actions | |
function setUp(state) { | |
const document = state.document | |
while (document.firstChild) { | |
document.removeChild(document.firstChild); | |
} | |
const body = document.createElement('body'); | |
document.appendChild(body); | |
const header = document.createElement('header'); | |
header.style.height = '100px'; | |
header.style.backgroundColor = "black" | |
body.appendChild(header); | |
const stateText = document.createElement('h3') | |
stateText.style.color = 'white' | |
stateText.innerText = state.state | |
header.appendChild(stateText) | |
const iframe = document.createElement('iframe'); | |
iframe.style.width = '100%'; | |
iframe.style.height = `${body.scrollHeight - header.scrollHeight}px` | |
iframe.addEventListener("load", () => dispatch(makeIFrameOnLoadEvent(detectPage(iframe.contentDocument)))) | |
body.appendChild(iframe); | |
state.bodyEl = body | |
state.headerEl = header | |
state.iframeEl = iframe | |
state.stateTextEl = stateText | |
} | |
function setStateText(state, text) { | |
if (state.stateTextEl == null) { | |
return | |
} | |
state.stateTextEl.innerText = text | |
} | |
function navigateToAppointments(iframe, appointmentsUrl) { | |
iframe.src = appointmentsUrl; | |
} | |
function startWaitTimer(state) { | |
state.waitTimerId = setTimeout(() => dispatch(makeWaitTimerEndEvent()), WAIT_DURATION_MS) | |
} | |
function clearWaitTimer(state) { | |
state.waitTimerId = clearTimeout(state.waitTimerId) | |
} | |
function alarmBeepStart(state) { | |
const beep = () => { | |
var context = new AudioContext(); | |
var oscillator = context.createOscillator(); | |
oscillator.type = "sine"; | |
oscillator.frequency.value = 800; | |
oscillator.connect(context.destination); | |
oscillator.start(); | |
// Beep for 500 milliseconds | |
setTimeout(function () { | |
oscillator.stop(); | |
}, 100); | |
} | |
state.alarmIntervalId = setInterval(beep, 1000) | |
} | |
function alarmBeepStop(state) { | |
state.alarmIntervalId = clearInterval(state.alarmIntervalId) | |
} | |
// FSM | |
function makeSetUpEvent(document, appointmentDatesPredicate) { | |
return { type: SET_UP_EVENT_TYPE, document, appointmentDatesPredicate } | |
} | |
function startEvent() { | |
return { type: START_EVENT_TYPE } | |
} | |
function makeIFrameOnLoadEvent(appointments_page_type) { | |
return { type: IFRAME_ON_LOAD_EVENT_TYPE, | |
appointmentsPageType: appointments_page_type} | |
} | |
function makeAppointmentsFoundEvent(availableDates) { | |
return { type: APPOINTMENTS_FOUND_EVENT_TYPE, availableDates } | |
} | |
function makeNoAppointmentsFoundEvent() { | |
return { type: NO_AVAILABLE_APPOINTMENTS_EVENT_TYPE } | |
} | |
function makeAppointmentsErrorEvent() { | |
return { type: APPOINTMENTS_ERROR_EVENT_TYPE } | |
} | |
function makeWaitTimerEndEvent() { | |
return { type: WAIT_TIMER_END_EVENT_TYPE } | |
} | |
function nextFSMState(state, event) { | |
const current_fsm_state = state.state | |
if (current_fsm_state === INITIAL_STATE && event.type === SET_UP_EVENT_TYPE) { | |
return SETTING_UP_STATE | |
} | |
if (current_fsm_state === SETTING_UP_STATE && event.type === START_EVENT_TYPE) { | |
return CHECKING_APPOINTMENTS_STATE | |
} | |
if (current_fsm_state === CHECKING_APPOINTMENTS_STATE) { | |
if (event.type === APPOINTMENTS_FOUND_EVENT_TYPE) { | |
return ALARM_STATE; | |
} | |
if (event.type === NO_AVAILABLE_APPOINTMENTS_EVENT_TYPE) { | |
return WAITING_STATE; | |
} | |
if (event.type === APPOINTMENTS_ERROR_EVENT_TYPE) { | |
return WAITING_STATE; | |
} | |
} | |
if (current_fsm_state === WAITING_STATE && event.type === WAIT_TIMER_END_EVENT_TYPE) { | |
return CHECKING_APPOINTMENTS_STATE; | |
} | |
} | |
function executeActions(state, event, current_fsm_state, next_fsm_state) { | |
// On Event | |
if (event.type === SET_UP_EVENT_TYPE) { | |
state.document = event.document | |
state.appointmentDatesPredicate = event.appointmentDatesPredicate | |
} | |
if (event.type === IFRAME_ON_LOAD_EVENT_TYPE && state.state === CHECKING_APPOINTMENTS_STATE) { | |
const page_type = event.appointmentsPageType | |
if (page_type === APPOINTMENTS_AVAILABLE_PAGE) { | |
const availableAppointmentDates = parseCalendars(state.iframeEl.contentDocument) | |
.filter(state.appointmentDatesPredicate) | |
if (availableAppointmentDates.length !== 0) { | |
dispatch(makeAppointmentsFoundEvent(availableAppointmentDates)) | |
} else { | |
dispatch(makeNoAppointmentsFoundEvent()) | |
} | |
} else if(page_type === APPOINTMENTS_UNAVAILABLE_PAGE) { | |
dispatch(makeNoAppointmentsFoundEvent()) | |
} else { | |
dispatch(makeAppointmentsErrorEvent()) | |
} | |
} | |
// On Leave | |
const leavingFSMState = next_fsm_state != null ? current_fsm_state : undefined | |
if (leavingFSMState === WAITING_STATE) { | |
clearWaitTimer(state) | |
} | |
if (leavingFSMState === ALARM_STATE) { | |
alarmBeepStop(state) | |
} | |
// On Transition | |
// On Enter | |
if (next_fsm_state !== undefined) { | |
console.info(`Entering state "${next_fsm_state}"`, {state, event, previous_fsm_state: current_fsm_state, next_fsm_state}) | |
setStateText(state, next_fsm_state) | |
} | |
if (next_fsm_state === SETTING_UP_STATE) { | |
setUp(state) | |
dispatch(startEvent()) | |
} | |
if (next_fsm_state === CHECKING_APPOINTMENTS_STATE) { | |
navigateToAppointments(state.iframeEl, APPOINTMENTS_CALENDAR_URL) | |
} | |
if (next_fsm_state === WAITING_STATE) { | |
startWaitTimer(state) | |
} | |
if (next_fsm_state === ALARM_STATE) { | |
alarmBeepStart(state) | |
} | |
// Change State | |
if (next_fsm_state !== undefined) { | |
state.state = next_fsm_state | |
} | |
} | |
const state = {state: INITIAL_STATE} | |
function dispatch(event) { | |
setTimeout(() => { | |
console.info(`Dispatching event "${event.type}"`, { event }) | |
const previous_fsm_state = state.state | |
const next_fsm_state = nextFSMState(state, event) | |
executeActions(state, event, previous_fsm_state, next_fsm_state) | |
if (next_fsm_state !== undefined) { | |
state.state = next_fsm_state | |
} | |
}, 0) | |
} | |
function filterAppointmentDates(date) { | |
return date.getMonth() === 10 && date.getDate() <= 14 | |
} | |
dispatch(makeSetUpEvent(document, filterAppointmentDates)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment