Files
Facilitor/UTILS/Exchange/exchange_graph.js

533 lines
27 KiB
JavaScript

/*
$Revision$
$Id$
File: exchange_graph.js
Description: Exchange MS Graph koppeling
Dit bestand maakt voor een ruimte-mailbox Arguments(0) de csv
met calenderitems aan sinds de laatste sync-actie, Arguments(1)
Result: Errorlevel 10 betekent alles goed
Errorlevel 1 betekent dat we een foutsituatie hebben ontdekt
Errorlevel 0 betekent dat er iets is fout gegaan wat we niet hebben onderkend
Parameters (0) e-mail adres van de zaal
(1) "EXCHFULL" calendarview-import (in de toekomst tot config.fullfuture)
alle andere waarden: synchronisatie-import (voorheen werd hier de syncstate in meegegeven)
*/
/* GLOBALS */
var fso = new ActiveXObject("Scripting.FileSystemObject");
/*////////////////////////////////
// //
// Main functions //
// //
////////////////////////////////*/
/*
In zaalemail res_ruimte_extern_id
import_app_code EXCHFULL of EXCHANGE(default)
as_stream teruggeven als fileStream, of wegschrijven naar een .csv bestand
Out Pre-processed import in csv formaat
*/
function generateCalendarImport(zaalemail, import_app_code, as_stream)
{
var deltatoken;
// Determine files wild card
var import_app_files = "sync_*.csv";
var import_app_files_pre = "sync_";
var sql = "SELECT i.fac_import_app_files "
+ " FROM fac_import_app i"
+ " WHERE i.fac_import_app_code = " + safe.quoted_sql(String(import_app_code));
var oRs = Oracle.Execute(sql);
if (!oRs.EOF)
{
import_app_files = oRs("fac_import_app_files").Value;
import_app_files_pre = import_app_files.split("*")[0].split(".")[0];
}
oRs.Close();
__Log("\n\n==== Room: " + zaalemail);
if (import_app_code != "EXCHFULL") {
deltatoken = getSyncToken(zaalemail);
} else {
// No skiptoken and no deltatoken means retrieve all events
}
var csvname = config.xmlfolder + import_app_files_pre + safefilename(zaalemail) + ".csv";
// Verwijderen: het verwijderen van onze oude sync_*.csv
try {
fso.DeleteFile(csvname);
__Log("Oude syncfile is verwijderd.");
} catch(e) {
// Neem aan dat -gelukkig- geen files zijn gevonden
}
__Log("Connecting to MS Graph API");
var response = getCalendarItems(zaalemail, null, deltatoken);
if (response && response.value)
{
var results = response.value;
while (response.skiptoken) {
response = getCalendarItems(zaalemail, response.skiptoken, null);
for (var index = 0; index < response.value.length; index++) {
results.push(response.value[index]);
}
}
if (!("deltatoken" in response)) {
// Het laatste antwoord bevatte geen deltatoken, dat hoort er altijd te zijn, waarschijnlijk een MS Graph fout.
return _handleMsGraphFailure(null, response);
}
// deltatoken needs to be saved (asap) to retrieve only changes during a future run
upsertSyncToken(zaalemail, response.deltatoken);
// De pre-processed data
var csvFileText = graphToImport(results, zaalemail);
// save the CSV file to disc here
fileStream = new ActiveXObject("ADODB.Stream");
fileStream.Type = 2; // adTypeBinary eerst nog
fileStream.Open();
fileStream.CharSet = "utf-8";
// save only if records are found
if (!!csvFileText) {
fileStream.WriteText(csvFileText);
if (as_stream) {
// Leveren we die zo op en gaat die rechtstreeks impReadStream in
} else {
fileStream.SaveToFile(csvname, 2); // overwrite
}
}
// Als we hier komen is alles goed
return fileStream;
}
}
/*////////////////////////////////
// //
// Helper functions //
// //
////////////////////////////////*/
/* De volgende 2 functies zoeken in de data (Array) die de calendarItems bevat */
function getSeriesMaster(data, seriesMasterId) {
var result = {};
for (var i = 0; i < data.length; i++) {
if (data[i].id == seriesMasterId) {
result = data[i];
break;
}
}
return result;
}
function getOccurrencesBySeriesMasterId(data, seriesMasterId) {
var result = [];
for (var i = 0; i < data.length; i++) {
if (data[i].seriesMasterId == seriesMasterId) {
result.push(data[i]);
}
}
return result;
}
/* Deze functie zoekt in de Facilitor DB */
function getReserveringByEvent(event, zaalemail)
{
// res_rsv_ruimte_externnr bestaat uit 2 delen, gescheiden door een '|'-teken;
// 1. Het hoofd-id; COALESCE(seriesMasterId, (eigen) id)
// 2. Als het event onderdeel is van een series, dan is het 2de deel het (eigen) id, anders leeg
//
// LET OP deze id's kunnen een '_' bevatten
var externnr;
var compareExternnrSql = "";
switch (event.type) {
case "occurrence":
case "exception":
compareExternnrSql = "rr.res_rsv_ruimte_externnr = " + safe.quoted_sql(event.seriesMasterId + "|" + event.id); // Exact
break;
case "singleInstance":
compareExternnrSql = "rr.res_rsv_ruimte_externnr = " + safe.quoted_sql(event.id + "|"); // Exact
break;
case "seriesMaster":
externnr = event.id.replace(/_/g, '\\_'); // Escape de underscore
compareExternnrSql = "rr.res_rsv_ruimte_externnr LIKE " + safe.quoted_sql(externnr + '|_%') + " ESCAPE '\\'"; // LIKE (de laatste underscore niet escapen)
break;
default: /* Een 'deleted' notificatie heeft geen type, dan kan het een occurrence zijn of niet, test met het event.id op beiden */
externnr = event.id.replace(/_/g, '\\_'); // Escape de underscore
compareExternnrSql = "rr.res_rsv_ruimte_externnr LIKE " + safe.quoted_sql("%" + externnr + "%") + " ESCAPE '\\'"; // LIKE
break;
}
if ("iCalUId" in event) { // Vanaf 2024.1 (met rich notifications) altijd zo
compareExternnrSql = "(" + compareExternnrSql + " OR rr.res_rsv_ruimte_externnr = " + safe.quoted_sql("##iCalUId##" + event.iCalUId + "|") + ")";
}
var eventSql = "SELECT rr.res_rsv_ruimte_key"
+ " , rr.res_rsv_ruimte_omschrijving"
+ " , rr.res_rsv_ruimte_van"
+ " , rr.res_rsv_ruimte_tot"
+ " , rr.res_rsv_ruimte_externnr"
+ " , rr.res_rsv_ruimte_externnr2"
+ " , p_host.prs_perslid_email"
+ " FROM res_rsv_ruimte rr,"
+ " res_ruimte_opstelling ro,"
+ " res_ruimte r,"
+ " prs_perslid p_host"
+ " WHERE rr.res_rsv_ruimte_externnr IS NOT NULL"
+ " AND " + compareExternnrSql
+ " AND rr.res_rsv_ruimte_verwijder IS NULL"
+ " AND ro.res_ruimte_opstel_key = rr.res_ruimte_opstel_key"
+ " AND r.res_ruimte_key = ro.res_ruimte_key"
+ " AND r.res_ruimte_verwijder IS NULL"
+ " AND rr.res_rsv_ruimte_host_key = p_host.prs_perslid_key"
+ " AND UPPER(r.res_ruimte_extern_id) = " + safe.quoted_sql_upper(zaalemail);
var eventoRs = Oracle.Execute(eventSql);
var result = [];
while (!eventoRs.eof) {
result.push({
"key": eventoRs("res_rsv_ruimte_key").Value,
"description": eventoRs("res_rsv_ruimte_omschrijving").Value,
"van": eventoRs("res_rsv_ruimte_van").Value,
"tot": eventoRs("res_rsv_ruimte_tot").Value,
"externnr": eventoRs("res_rsv_ruimte_externnr").Value || "",
"organisatorId":eventoRs("res_rsv_ruimte_externnr2").Value || "",
"host_mail": eventoRs("prs_perslid_email").Value || ""
});
if (result[result.length - 1].externnr.slice(0, 11) === "##iCalUId##") { // Werk het FCLT-record direct bij zodat de import hem straks ook kan vinden
updateFcltRecord(result[result.length - 1].key, event, result[result.length - 1].organisatorId);
}
eventoRs.moveNext();
}
eventoRs.Close();
return result;
}
/*
In Array van calendarItems
Out Pre-processed import in csv formaat
*/
function graphToImport(data, zaalemail)
{
var trs = [];
var tds = [];
var index;
var masterEvent;
var curDate = new Date();
var seqNbr = curDate.getTime();
var millisPerDay = 24 * 60 * 60 * 1000;
var maxPastMS = curDate.setHours(0, 0, 0, 0) - (config.fullpast * millisPerDay);
var maxFutureMS = curDate.setHours(23, 59, 59, 999) + (config.fullfuture * millisPerDay);
var SUBJECT_MAX_LENGTH = 60;
var _roomFallbackEmail;
var _unknownFallbackEmail;
var lcl_res_rsv_private = "";
if (data.length) {
var sql = "SELECT prs_perslid_email FROM prs_v_aanwezigperslid WHERE prs_perslid_oslogin = " + safe.quoted_sql_upper("_MSGRAPH_FALLBACK_ROOMS");
var oRs = Oracle.Execute(sql);
if (!oRs.EoF) {
_roomFallbackEmail = oRs("prs_perslid_email").Value;
}
oRs.Close();
sql = "SELECT prs_perslid_email FROM prs_v_aanwezigperslid WHERE prs_perslid_oslogin = " + safe.quoted_sql_upper("_MSGRAPH_FALLBACK_UNKNOWN");
oRs = Oracle.Execute(sql);
if (!oRs.EoF) {
_unknownFallbackEmail = oRs("prs_perslid_email").Value;
}
oRs.Close();
sql = "SELECT lcl.l('lcl_res_rsv_private') lcl_res_rsv_private FROM DUAL";
oRs = Oracle.Execute(sql);
if (!oRs.EoF) {
lcl_res_rsv_private = oRs("lcl_res_rsv_private").Value;
}
oRs.Close();
}
// Past de organizer.emailAddress.address aan van het meegegeven event indien nodig
function validateOrganizer(event, occurrence) {
// Als de organisator niet als prs_perslid in Facilitor bekend is, kennen we 2 fallback-accounts;
// 1) De organisator is een ruimte; externe software boekt vaak direct in de agenda vd ruimte, zoals Yealink, maar ook Facilitor zelf (bij msgraph_sync_level & 2)
// 2) De organisator is onbekend; technisch/funcioneel account, of gewoon onbekend in Facilitor
// Als de relevante fallback-account niet (juist) is geconfigureerd, returnen we een falsy value en wordt de regel niet geimporteerd
if ((event.organizer.emailAddress.address || "").slice(-2) === "##") {
// Deze hebben we al aangepast
} else if (event.isOrganizer) { // Ruimte = organizer (Yealink)
event.organizer.emailAddress.address = _roomFallbackEmail;
} else { // Try to find host by email
sql = "SELECT COUNT (*) aantal FROM prs_v_aanwezigperslid WHERE UPPER(prs_perslid_email) = " + safe.quoted_sql_upper(event.organizer.emailAddress.address);
oRs = Oracle.Execute(sql);
if (oRs("aantal").Value !== 1) { // Unknown or non-unique organizer
if (_unknownFallbackEmail) { // _unknownFallbackEmail is gedefinieerd en dus geconfigureerd
event.organizer.emailAddress.address = "##" + event.organizer.emailAddress.address + "##"; // Deze pakt de package op
} else {
event.organizer.emailAddress.address = null;
}
}
oRs.Close();
}
if (typeof occurrence != "undefined" && occurrence.type === "exception") { // Dan staat hier de(zelfde) organisator
occurrence.organizer.emailAddress.address = event.organizer.emailAddress.address;
}
return event.organizer.emailAddress.address;
}
// Header
var headerRow = [
"Subject",
"StartTime",
"EndTime",
"Organizer",
"Email",
"Name",
"Modifier",
"ApptId",
"RecurId",
"SeqNr"
].join(";");
// rows in data
/* types description
=============== ===============================
singleInstance: stand alone event
seriesMaster: definition of a repeating event
occurrence: repeating event occurrence
exception: changed repeating occurrence
*/
for (index = 0; index < data.length; index++)
{
var thisEvent = data[index];
// Eerst wat uitzonderingen eruit filteren
if ((getMSGraphSyncLevel() & 4) && "responseStatus" in thisEvent && thisEvent.responseStatus.response == "notResponded")
{ /*
Bij S("msgraph_sync_level") & 2 is dit "organizer" [Deze is vanuit Facilitor, direct op ruimte in Outlook geboekt]
Bij Yealink ad-hoc reserveringen is dit ook "organizer" [Deze is vanuit Yealink, direct op ruimte in Outlook geboekt]
Bij S("msgraph_sync_level") & 4 is dit de eerste keer "notResponded", en direct daarna "accepted"
*/
continue; // Hier doen we (nog) niets mee
}
var reserveringenFacilitor = getReserveringByEvent(thisEvent, zaalemail) || []; // Dit levert meerdere records op bij type = seriesMaster
if (( (getMSGraphSyncLevel() & 2) == 2 && // Als wij zelf afspraken op naam van de ruimte boeken, willen we zulke events nooit importeren
( (reserveringenFacilitor.length > 0 && reserveringenFacilitor[0].organisatorId == thisEvent.id) || // Deze is bekend in Facilitor en de ruimte is de organisator
(reserveringenFacilitor.length === 0 && /* Ooit; !_roomFallbackEmail && // Als je de fallback-user gebruikt dan is dit supported, anders niet */
(thisEvent.isOrganizer || // Deze is niet bekend in Facilitor en de ruimte is de organisator
inArray(thisEvent.type, ["occurrence", "exception"]) &&
(getSeriesMaster(data, thisEvent.seriesMasterId) || {}).isOrganizer))))) { // idem, maar dan bij de seriesMaster
// Deze slaan we over
continue;
}
// Facilitor-initiated reserveringen beginnen met alleen het externnr2 gevuld, update voor zover mogelijk hier het externnr
if (getMSGraphSyncLevel() & 4 && reserveringenFacilitor.length === 0) { // ReadWrite
var updateResult = updateExternnr(zaalemail, thisEvent.id);
if (updateResult == -1) { // De reservering is niet gevonden in Facilitor, en ook is de reservering niet geaccepteerd; dan hoeven we hier dus (nog) niets mee
continue; // Errorcode; skip record (Ruimte heeft nog niet geaccepteerd)
} else if (updateResult) {
reserveringenFacilitor = getReserveringByEvent(thisEvent, zaalemail) || []; // Probeer nog eens
}
}
// [D]eleted events
if (thisEvent["@removed"] || thisEvent.isCancelled || (("responseStatus" in thisEvent) && thisEvent.responseStatus.response == "declined"))
{
if (thisEvent.type == "seriesMaster") {
continue; // We krijgen per occurrence ook een event binnen en verwijderen die dan
}
// Op dit moment kan het nog steeds een "@removed" seriesMaster zijn, dan moeten we direct de hele serie deleten want we krijgen geen losse notificaties van de occurrences
for (var f in reserveringenFacilitor) {
if (new Date(reserveringenFacilitor[f].tot).getTime() >= maxPastMS &&
new Date(reserveringenFacilitor[f].van).getTime() <= maxFutureMS) {
if (("type" in thisEvent) && thisEvent.type === "singleInstance") { // Sla dan het iCalUId op zodat we deze sneller kunnen terugvinden bij een evt. ruimte-wissel
updateFcltRecord(reserveringenFacilitor[f].key, thisEvent, null, { "iCalUId": true, "force": false });
var sql = "SELECT res_rsv_ruimte_externnr FROM res_rsv_ruimte WHERE res_rsv_ruimte_key = " + reserveringenFacilitor[f].key;
var oRs = Oracle.Execute(sql);
var newExternnr = oRs.EoF
? reserveringenFacilitor[f].externnr
: oRs("res_rsv_ruimte_externnr").Value;
oRs.Close();
var eventId = newExternnr.split("|")[0];
var occurrenceId = newExternnr.split("|")[1] || "";
if (eventId.slice(0, 11) === "##iCalUId##") {
// Verwijder de id's van de oude verwijderde 'backups', die zijn niet meer nodig
var sql = "UPDATE res_rsv_ruimte"
+ " SET res_rsv_ruimte_externnr = NULL"
+ " WHERE res_rsv_ruimte_externnr = " + safe.quoted_sql(eventId + "|")
+ " AND res_rsv_ruimte_verwijder IS NOT NULL";
Oracle.Execute(sql);
}
} else {
var eventId = reserveringenFacilitor[f].externnr.split("|")[0];
var occurrenceId = reserveringenFacilitor[f].externnr.split("|")[1] || "";
}
tds = [
"",
"",
"",
"",
"",
"",
"",
"D", // [D]elete
safe.csv(eventId),
safe.csv(occurrenceId),
++seqNbr
];
trs.push(tds.join(";"));
}
}
}
else
{
var _startDate = parseISOString(thisEvent.start.dateTime);
var _endDate = parseISOString(thisEvent.end.dateTime);
if (_endDate.getTime() >= maxPastMS && _startDate.getTime() <= maxFutureMS)
{
{
var modifier = reserveringenFacilitor.length === 0 ? "C" : "U"; // [U]pdate of [C]reate
if (thisEvent.type == "singleInstance") // Simpel, single event
{
if (!validateOrganizer(thisEvent)) {
continue;
}
if (!eventReadyForProcessing(zaalemail, thisEvent)) {
continue;
}
var visibility = thisEvent.sensitivity === "normal" ? 1 : 0; // sensitivity IN ["normal", "personal", "private", "confidential"], we interpreteren die laatste 3 hetzelfde
tds = [
safe.csv((visibility ? thisEvent.subject : lcl_res_rsv_private).slice(0, SUBJECT_MAX_LENGTH)),
_startDate.toISOString(),
_endDate.toISOString(),
safe.csv(thisEvent.organizer.emailAddress.address),
"",
"",
safe.csv(visibility),
modifier,
safe.csv(thisEvent.id),
"",
++seqNbr
];
}
else if (inArray(thisEvent.type, ["occurrence", "exception"])) // Part of event-series (recurring)
{
masterEvent = getSeriesMaster(data, thisEvent.seriesMasterId) || {};
if (masterEvent.isCancelled) {
tds = [
"",
"",
"",
"",
"",
"",
"",
"D", // cancelled or deleted
safe.csv(thisEvent.seriesMasterId),
safe.csv(thisEvent.id),
++seqNbr
];
trs.push(tds.join(";"));
continue;
} else {
if (!validateOrganizer(masterEvent, thisEvent)) {
continue;
}
if (!eventReadyForProcessing(zaalemail, masterEvent)) {
continue;
}
var visibility = masterEvent.sensitivity === "normal" ? 1 : 0; // Gebruik de gegevens van de seriesMaster
tds = [
safe.csv((visibility ? masterEvent.subject : lcl_res_rsv_private).slice(0, SUBJECT_MAX_LENGTH)),
_startDate.toISOString(),
_endDate.toISOString(),
safe.csv(masterEvent.organizer.emailAddress.address),
"",
"",
safe.csv(visibility),
modifier,
safe.csv(thisEvent.seriesMasterId),
safe.csv(thisEvent.id),
++seqNbr
];
}
}
else if (thisEvent.type === "seriesMaster")
{ // Voer een (import) delete uit op alle occurrences van de seriesMaster die in Facilitor voorkomen maar niet in MS Graph
// Creates en Updates gaan vanzelf goed omdat dan de occurrence in de calendarView-response (=data) is meegegeven
// Bij deletes is dat niet het geval
// Deze calendarView/delta omvat de gehele serie, dus als een occurrence hier niet in staat, bestaat die niet (meer)
var reserveringenOutlook = getOccurrencesBySeriesMasterId(data, thisEvent.id);
for (var f in reserveringenFacilitor) {
var found = false;
for (var o in reserveringenOutlook) {
if (reserveringenOutlook[o].seriesMasterId === reserveringenFacilitor[f].externnr.split("|")[0] && // Match op seriesMasterId
reserveringenOutlook[o].id === reserveringenFacilitor[f].externnr.split("|")[1]) { // en op het id vd occurrence
found = true;
break;
}
}
if (!found) {
tds = [
"",
"",
"",
"",
"",
"",
"",
"D", // [D]eleted
safe.csv(reserveringenFacilitor[f].externnr.split("|")[0]),
safe.csv(reserveringenFacilitor[f].externnr.split("|")[1]),
++seqNbr
];
trs.push(tds.join(";"));
}
}
continue; // Klaar
}
trs.push(tds.join(";"));
if ( thisEvent.type === "occurrence" && "attendees" in masterEvent // Deelnemers van occurrences staan bij de seriesMaster geregistreerd
|| "attendees" in thisEvent)
{ // Volgens mij zijn exceptions hier nooit relevant; ze zijn nooit nieuw (want ze begonnen als occurrence) en dus registreren we geen bezoeker-mutaties
var eventId = inArray(thisEvent.type, ["occurrence", "exception"]) ? thisEvent.seriesMasterId : thisEvent.id;
var occurrenceId = inArray(thisEvent.type, ["occurrence", "exception"]) ? thisEvent.id : "";
var thisEvent = ("attendees" in thisEvent) ? thisEvent : masterEvent; // Gebruik bij occurrence de gegevens van de seriesMaster
var visibility = thisEvent.sensitivity === "normal" ? 1 : 0;
for (var attendee in thisEvent.attendees) {
if (thisEvent.attendees[attendee].emailAddress &&
thisEvent.attendees[attendee].emailAddress.address &&
thisEvent.attendees[attendee].emailAddress.address !== thisEvent.organizer.emailAddress.address && // Organisator hoeft niet
thisEvent.attendees[attendee].emailAddress.address.toUpperCase() !== zaalemail.toUpperCase()) { // Ruimte ook niet
tds = [
safe.csv((visibility ? thisEvent.subject : lcl_res_rsv_private).slice(0, SUBJECT_MAX_LENGTH)),
_startDate.toISOString(),
_endDate.toISOString(),
safe.csv(thisEvent.organizer.emailAddress.address),
safe.csv(thisEvent.attendees[attendee].emailAddress.address),
safe.csv(thisEvent.attendees[attendee].emailAddress.name),
safe.csv(visibility),
modifier,
safe.csv(eventId),
safe.csv(occurrenceId),
seqNbr
];
trs.push(tds.join(";"));
}
}
}
}
}
}
}
if (trs.length)
trs = [headerRow].concat(trs);
return trs.join("\r\n");
}