Skip to content

Instantly share code, notes, and snippets.

@thevtm
Last active November 3, 2023 00:33
Show Gist options
  • Save thevtm/042e4e7289c86c6eccd01d19117da191 to your computer and use it in GitHub Desktop.
Save thevtm/042e4e7289c86c6eccd01d19117da191 to your computer and use it in GitHub Desktop.
Berlin Appointments Alert
// 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