From 604ab1e6f320edc3f65ec2e5da13336d4427162d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Zieren?= Date: Fri, 28 Apr 2023 11:38:13 +0200 Subject: [PATCH] Notify about removed events (fixes #26) --- main.js | 66 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/main.js b/main.js index 9726823..cd8fd4a 100644 --- a/main.js +++ b/main.js @@ -63,6 +63,13 @@ const EMPTY_STATE = { const INQUIRY_AUTHOR = ['Eltern', 'Klassenleitung', 'UNKNOWN']; +// Status of events and their (partial) HTML representation. +const STATUS_TO_HTML = { + '-1': '--', // removed (a rare but relevant case) + '0': '', // previously notified + '1': '*', // added +}; + /** * List of already processed (i.e. emailed) items. Contains the following keys: * - 'announcements': Announcements in "Aktuelles". @@ -595,11 +602,13 @@ async function readEventsInternal(page) { async function readEvents(page, previousEvents) { // An event is considered expired on the next day. We store events with a time of day of 0:00:00, - // so we compute the timestamp for 0:00:00 today and prune events before then. + // so we compute the timestamp for 0:00:00 today and prune events before then. Note that the event + // HTML also contains the date, so using it as a key is sufficient and we can ignore the + // timestamp. const todayZeroDate = new Date(NOW); todayZeroDate.setHours(0, 0, 0, 0); const todayZeroTs = todayZeroDate.getTime(); - Object.entries(previousEvents) + Object.entries(previousEvents) // yields array of [html, ts] tuples .filter(([_, ts]) => ts < todayZeroTs) .forEach(([html, _]) => delete previousEvents[html]) @@ -613,14 +622,28 @@ async function readEvents(page, previousEvents) { let lookaheadDate = new Date(todayZeroDate); lookaheadDate.setDate(lookaheadDate.getDate() + CONFIG.options.eventLookaheadDays); const lookaheadTs = lookaheadDate.getTime(); - const upcomingEvents = - events.filter(e => e.ts >= todayZeroTs && e.ts <= lookaheadTs).sort((a, b) => a.ts - b.ts); - const numNewEvents = upcomingEvents.filter(e => !(e.html in previousEvents)).length; - - LOG.info('New upcoming events: %d', numNewEvents); - - // Create emails. - if (!numNewEvents) { + let upcomingEvents = events + .filter(e => e.ts >= todayZeroTs && e.ts <= lookaheadTs) + // See STATUS_TO_HTML for status codes. + .map(e => { return {...e, status: e.html in previousEvents ? 0 : 1 }; }); + const numNewEvents = upcomingEvents.filter(e => e.status == 1).length; + + // Find removed events. previousEvents has been pruned above, so anything it contains that is no + // longer upcoming was removed. + const upcomingEventsHtml = upcomingEvents.map(e => e.html); + const removedEvents = Object.entries(previousEvents) // yields array of [html, ts] tuples + .filter(([html, _]) => !upcomingEventsHtml.includes(html)) + .map(([html, ts]) => { return { html: html, ts: ts, status: -1 /* means: removed */}; }); + const numRemovedEvents = Object.keys(removedEvents).length; + + // Join the two and sort them by timestamp. + upcomingEvents = upcomingEvents.concat(removedEvents).sort((a, b) => a.ts - b.ts); + + LOG.info(`${upcomingEvents.length} upcoming event(s), ` + + `of which ${numNewEvents} new and ${numRemovedEvents} removed`); + + // Create emails. + if (!(numNewEvents + numRemovedEvents)) { return; } let emailHTML = 'Bevorstehende Termine' @@ -628,29 +651,36 @@ async function readEvents(page, previousEvents) { + 'table { border-collapse: collapse; } ' + 'tr { border-bottom: 1pt solid; } ' + 'tr.new { font-weight: bold; } ' + + 'tr.removed { text-decoration: line-through; } ' + '' + '

Termine in den nächsten ' + CONFIG.options.eventLookaheadDays + ' Tagen

'; - upcomingEvents.forEach(e => emailHTML += - (e.html in previousEvents ? '' - + e.html + ''); + upcomingEvents.forEach(e => emailHTML += STATUS_TO_HTML[e.status] + '' + e.html + ''); emailHTML += '
' : '
*') + '
'; const doStudent = !!CONFIG.options.emailToStudent; let emailsLeft = doStudent ? 2 : 1; + + const okHandler = function() { + // Update state of previous (announced) events when all emails are sent. + if (!--emailsLeft) upcomingEvents.forEach(e => { + if (e.status == 1) { + previousEvents[e.html] = e.ts; // new event -> no longer new next time + } else if (e.status == -1) { + delete previousEvents[e.html]; // removed event -> no longer included next time + } // else: status 0 means the event exists both in the portal and in previousEvents -> no-op + })}; + inbound.push({ email: buildEmail('Bevorstehende Termine', 'Bevorstehende Termine', {html: emailHTML}), - // Mark event as "previous" (i.e. announced) when no more emails are left to send. - ok: () => { if (!--emailsLeft) upcomingEvents.forEach(e => previousEvents[e.html] = e.ts); } + ok: () => okHandler() }); if (doStudent) { inbound.push({ email: buildEmail('Bevorstehende Termine', 'Bevorstehende Termine', {html: emailHTML, to: CONFIG.options.emailToStudent}), - // Same as ok() above. - ok: () => { if (!--emailsLeft) upcomingEvents.forEach(e => previousEvents[e.html] = e.ts); } + ok: () => okHandler() }); } - LOG.info('%d upcoming event(s), of which %d new', upcomingEvents.length, numNewEvents); } // ---------- General email functions ----------