@things-factory/integration-msgraph 10.1.257 → 10.1.259
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-server/engine/task/index.d.ts +1 -0
- package/dist-server/engine/task/index.js +1 -0
- package/dist-server/engine/task/index.js.map +1 -1
- package/dist-server/engine/task/msgraph-calendar-meetings.d.ts +61 -0
- package/dist-server/engine/task/msgraph-calendar-meetings.js +170 -0
- package/dist-server/engine/task/msgraph-calendar-meetings.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/helps/integration/task/msgraph-calendar-meetings.md +40 -0
- package/package.json +3 -3
- package/translations/en.json +5 -1
- package/translations/es.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +6 -2
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/engine/task/index.ts"],"names":[],"mappings":";;AAAA,yBAAsB;AACtB,0BAAuB;AACvB,yCAAsC;AACtC,sCAAmC","sourcesContent":["import './msgraph-get'\nimport './msgraph-post'\nimport './msgraph-license-assignments'\nimport './msgraph-mailbox-messages'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/engine/task/index.ts"],"names":[],"mappings":";;AAAA,yBAAsB;AACtB,0BAAuB;AACvB,yCAAsC;AACtC,sCAAmC;AACnC,uCAAoC","sourcesContent":["import './msgraph-get'\nimport './msgraph-post'\nimport './msgraph-license-assignments'\nimport './msgraph-mailbox-messages'\nimport './msgraph-calendar-meetings'\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface MeetingAttendee {
|
|
2
|
+
email: string;
|
|
3
|
+
name: string | null;
|
|
4
|
+
/** The invitation's answer: accepted, tentativelyAccepted, declined, notResponded, organizer or none. */
|
|
5
|
+
response: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface MeetingAttendance {
|
|
8
|
+
email: string;
|
|
9
|
+
seconds: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CalendarMeeting {
|
|
12
|
+
/** Whose calendar it was read from. */
|
|
13
|
+
mailbox: string;
|
|
14
|
+
/** The occurrence's id across calendars (iCalUId) - one per occurrence of a series too. */
|
|
15
|
+
id: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
start: string;
|
|
18
|
+
end: string;
|
|
19
|
+
organizer: string | null;
|
|
20
|
+
attendees: MeetingAttendee[];
|
|
21
|
+
online: boolean;
|
|
22
|
+
location: string | null;
|
|
23
|
+
/** The mailbox owner's own answer to it. */
|
|
24
|
+
response: string | null;
|
|
25
|
+
/** How long each person was in it - a Teams meeting that has ended, when its report could be read; else null. */
|
|
26
|
+
attendance: MeetingAttendance[] | null;
|
|
27
|
+
/** Cancelled there - handed on so a calendar that brought it in cancels it too. */
|
|
28
|
+
cancelled: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** What a Graph event says, as the keeper of meetings reads it - null for a private one or one with nobody else in it. */
|
|
31
|
+
export declare function meetingOf(mailbox: string, raw: any): CalendarMeeting | null;
|
|
32
|
+
/**
|
|
33
|
+
* How long each person was in a Teams meeting: its organizer's online meeting found by the join address, the report
|
|
34
|
+
* of the occurrence that began within the event, and that report's records. Null when any of it cannot be read.
|
|
35
|
+
*/
|
|
36
|
+
export declare function attendanceOf(client: any, organizer: string, joinUrl: string, start: string, end: string): Promise<MeetingAttendance[] | null>;
|
|
37
|
+
/**
|
|
38
|
+
* What one reading found: the window it read, the mailboxes it could read and those it could not, and the meetings.
|
|
39
|
+
* A calendar that brings them in may take a meeting of the window it no longer finds in a mailbox read as removed there.
|
|
40
|
+
*/
|
|
41
|
+
export interface CalendarReading {
|
|
42
|
+
from: string;
|
|
43
|
+
to: string;
|
|
44
|
+
mailboxes: string[];
|
|
45
|
+
failed: {
|
|
46
|
+
mailbox: string;
|
|
47
|
+
reason: string;
|
|
48
|
+
}[];
|
|
49
|
+
meetings: CalendarMeeting[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The meetings of each mailbox between `from` and `to`, each Teams meeting that has ended with its attendance. A mailbox
|
|
53
|
+
* that cannot be read - one with no calendar, a person the access policy leaves out - is named in `failed` and the rest
|
|
54
|
+
* are read; when none can be read the reading fails, for then it is the permission, not a person.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readCalendarMeetings(client: any, mailboxes: string[], from: string, to: string, now?: number): Promise<CalendarReading>;
|
|
57
|
+
/**
|
|
58
|
+
* The mailboxes named by a list: addresses, or records that carry one - an employee list read in an earlier step
|
|
59
|
+
* (`{ active, user: { email } }`) gives the address of each active one with a user.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mailboxesOf(listed: unknown): string[];
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.meetingOf = meetingOf;
|
|
4
|
+
exports.attendanceOf = attendanceOf;
|
|
5
|
+
exports.readCalendarMeetings = readCalendarMeetings;
|
|
6
|
+
exports.mailboxesOf = mailboxesOf;
|
|
7
|
+
const integration_base_1 = require("@things-factory/integration-base");
|
|
8
|
+
const address = (one) => String(one?.emailAddress?.address ?? '').toLowerCase();
|
|
9
|
+
const utc = (one) => (one?.dateTime ? new Date(`${String(one.dateTime).replace(/Z$/, '')}Z`).toISOString() : '');
|
|
10
|
+
/** What a Graph event says, as the keeper of meetings reads it - null for a private one or one with nobody else in it. */
|
|
11
|
+
function meetingOf(mailbox, raw) {
|
|
12
|
+
if (!raw?.iCalUId)
|
|
13
|
+
return null;
|
|
14
|
+
if (['private', 'confidential'].includes(String(raw.sensitivity ?? '').toLowerCase()))
|
|
15
|
+
return null;
|
|
16
|
+
const attendees = (raw.attendees ?? [])
|
|
17
|
+
.map((one) => ({ email: address(one), name: one?.emailAddress?.name ?? null, response: one?.status?.response ?? null }))
|
|
18
|
+
.filter((one) => one.email);
|
|
19
|
+
const organizer = address(raw.organizer) || null;
|
|
20
|
+
/* A meeting has someone other than the mailbox's owner in it. */
|
|
21
|
+
const others = new Set([organizer, ...attendees.map(one => one.email)].filter(one => one && one !== mailbox));
|
|
22
|
+
if (!others.size)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
mailbox,
|
|
26
|
+
id: raw.iCalUId,
|
|
27
|
+
subject: String(raw.subject ?? ''),
|
|
28
|
+
start: utc(raw.start),
|
|
29
|
+
end: utc(raw.end),
|
|
30
|
+
organizer,
|
|
31
|
+
attendees,
|
|
32
|
+
online: !!raw.isOnlineMeeting && String(raw.onlineMeetingProvider ?? '') === 'teamsForBusiness',
|
|
33
|
+
location: raw.location?.displayName || null,
|
|
34
|
+
response: raw.responseStatus?.response ?? null,
|
|
35
|
+
attendance: null,
|
|
36
|
+
cancelled: !!raw.isCancelled
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const FIELDS = ['iCalUId', 'subject', 'start', 'end', 'organizer', 'attendees', 'isOnlineMeeting', 'onlineMeetingProvider', 'onlineMeeting', 'sensitivity', 'isCancelled', 'location', 'responseStatus'];
|
|
40
|
+
async function pages(client, request) {
|
|
41
|
+
const out = [];
|
|
42
|
+
let page = await request.get();
|
|
43
|
+
for (;;) {
|
|
44
|
+
out.push(...(page?.value ?? []));
|
|
45
|
+
const next = page?.['@odata.nextLink'];
|
|
46
|
+
if (!next)
|
|
47
|
+
return out;
|
|
48
|
+
page = await client.api(next).header('Prefer', 'outlook.timezone="UTC"').get();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* How long each person was in a Teams meeting: its organizer's online meeting found by the join address, the report
|
|
53
|
+
* of the occurrence that began within the event, and that report's records. Null when any of it cannot be read.
|
|
54
|
+
*/
|
|
55
|
+
async function attendanceOf(client, organizer, joinUrl, start, end) {
|
|
56
|
+
try {
|
|
57
|
+
const user = `/users/${encodeURIComponent(organizer)}`;
|
|
58
|
+
const found = await client.api(`${user}/onlineMeetings`).filter(`JoinWebUrl eq '${joinUrl.replace(/'/g, "''")}'`).get();
|
|
59
|
+
const meeting = found?.value?.[0];
|
|
60
|
+
if (!meeting?.id)
|
|
61
|
+
return null;
|
|
62
|
+
const reports = (await client.api(`${user}/onlineMeetings/${meeting.id}/attendanceReports`).get())?.value ?? [];
|
|
63
|
+
/* A series has a report per occurrence; this occurrence's began between an hour before it and its end. */
|
|
64
|
+
const from = Date.parse(start) - 3600000;
|
|
65
|
+
const until = Date.parse(end);
|
|
66
|
+
const report = reports.find(one => {
|
|
67
|
+
const began = Date.parse(one.meetingStartDateTime);
|
|
68
|
+
return began >= from && began <= until;
|
|
69
|
+
});
|
|
70
|
+
if (!report)
|
|
71
|
+
return null;
|
|
72
|
+
const records = await pages(client, client.api(`${user}/onlineMeetings/${meeting.id}/attendanceReports/${report.id}/attendanceRecords`));
|
|
73
|
+
const seconds = new Map();
|
|
74
|
+
for (const one of records) {
|
|
75
|
+
const email = String(one.emailAddress ?? '').toLowerCase();
|
|
76
|
+
if (email)
|
|
77
|
+
seconds.set(email, (seconds.get(email) ?? 0) + (Number(one.totalAttendanceInSeconds) || 0));
|
|
78
|
+
}
|
|
79
|
+
return [...seconds].map(([email, total]) => ({ email, seconds: total }));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The meetings of each mailbox between `from` and `to`, each Teams meeting that has ended with its attendance. A mailbox
|
|
87
|
+
* that cannot be read - one with no calendar, a person the access policy leaves out - is named in `failed` and the rest
|
|
88
|
+
* are read; when none can be read the reading fails, for then it is the permission, not a person.
|
|
89
|
+
*/
|
|
90
|
+
async function readCalendarMeetings(client, mailboxes, from, to, now = Date.now()) {
|
|
91
|
+
const reading = { from: new Date(from).toISOString(), to: new Date(to).toISOString(), mailboxes: [], failed: [], meetings: [] };
|
|
92
|
+
const attendance = new Map();
|
|
93
|
+
for (const raw of mailboxes) {
|
|
94
|
+
const mailbox = String(raw ?? '').trim().toLowerCase();
|
|
95
|
+
if (!/^[^@\s]+@[^@\s]+$/.test(mailbox))
|
|
96
|
+
throw new Error(`'${raw}' is not a mailbox address`);
|
|
97
|
+
let events;
|
|
98
|
+
try {
|
|
99
|
+
events = await pages(client, client
|
|
100
|
+
.api(`/users/${encodeURIComponent(mailbox)}/calendarView`)
|
|
101
|
+
.header('Prefer', 'outlook.timezone="UTC"')
|
|
102
|
+
.query({ startDateTime: reading.from, endDateTime: reading.to })
|
|
103
|
+
.select(FIELDS)
|
|
104
|
+
.top(100));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const code = error?.code ? ` (${error.code})` : '';
|
|
108
|
+
reading.failed.push({ mailbox, reason: `${error?.message ?? error}${code}` });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
reading.mailboxes.push(mailbox);
|
|
112
|
+
for (const event of events) {
|
|
113
|
+
const meeting = meetingOf(mailbox, event);
|
|
114
|
+
if (!meeting)
|
|
115
|
+
continue;
|
|
116
|
+
const joinUrl = event.onlineMeeting?.joinUrl;
|
|
117
|
+
if (!meeting.cancelled && meeting.online && joinUrl && meeting.organizer && Date.parse(meeting.end) < now) {
|
|
118
|
+
if (!attendance.has(meeting.id))
|
|
119
|
+
attendance.set(meeting.id, attendanceOf(client, meeting.organizer, joinUrl, meeting.start, meeting.end));
|
|
120
|
+
meeting.attendance = await attendance.get(meeting.id);
|
|
121
|
+
}
|
|
122
|
+
reading.meetings.push(meeting);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (mailboxes.length && !reading.mailboxes.length) {
|
|
126
|
+
const first = reading.failed[0];
|
|
127
|
+
throw new Error(`no calendar could be read - ${first.mailbox}: ${first.reason} - needs the application permission Calendars.ReadBasic.All with admin consent, allowed for these mailboxes`);
|
|
128
|
+
}
|
|
129
|
+
return reading;
|
|
130
|
+
}
|
|
131
|
+
const read = (data, accessor) => (accessor ? String(accessor).split('.').reduce((value, key) => value?.[key], data) : undefined);
|
|
132
|
+
/**
|
|
133
|
+
* The mailboxes named by a list: addresses, or records that carry one - an employee list read in an earlier step
|
|
134
|
+
* (`{ active, user: { email } }`) gives the address of each active one with a user.
|
|
135
|
+
*/
|
|
136
|
+
function mailboxesOf(listed) {
|
|
137
|
+
const items = Array.isArray(listed) ? listed : String(listed ?? '').split(/[,\s]+/);
|
|
138
|
+
const out = new Set();
|
|
139
|
+
for (const one of items) {
|
|
140
|
+
if (one && typeof one === 'object' && one.active === false)
|
|
141
|
+
continue;
|
|
142
|
+
const address = typeof one === 'string' ? one : one?.email ?? one?.user?.email;
|
|
143
|
+
if (address && String(address).trim())
|
|
144
|
+
out.add(String(address).trim().toLowerCase());
|
|
145
|
+
}
|
|
146
|
+
return [...out];
|
|
147
|
+
}
|
|
148
|
+
async function MsGraphCalendarMeetings(step, context) {
|
|
149
|
+
const { connection: connectionName, params } = step;
|
|
150
|
+
const { domain, data } = context;
|
|
151
|
+
const client = await integration_base_1.ConnectionManager.getConnectionInstanceByName(domain, connectionName);
|
|
152
|
+
if (!client)
|
|
153
|
+
throw new Error(`no connection '${connectionName}'`);
|
|
154
|
+
const { mailboxes, mailboxesAccessor, daysBefore, daysAfter } = params || {};
|
|
155
|
+
/* The people: given, or read off an earlier step (the keeper's list of mailboxes). */
|
|
156
|
+
const people = mailboxesOf(read(data, mailboxesAccessor) ?? mailboxes ?? []);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const from = new Date(now - (Number(daysBefore) || 14) * 86400000).toISOString();
|
|
159
|
+
const to = new Date(now + (Number(daysAfter) || 14) * 86400000).toISOString();
|
|
160
|
+
return { data: await readCalendarMeetings(client, people, from, to, now) };
|
|
161
|
+
}
|
|
162
|
+
MsGraphCalendarMeetings.parameterSpec = [
|
|
163
|
+
{ type: 'string', name: 'mailboxes', label: 'mailboxes' },
|
|
164
|
+
{ type: 'string', name: 'mailboxesAccessor', label: 'mailboxes-accessor' },
|
|
165
|
+
{ type: 'number', name: 'daysBefore', label: 'days-before' },
|
|
166
|
+
{ type: 'number', name: 'daysAfter', label: 'days-after' }
|
|
167
|
+
];
|
|
168
|
+
MsGraphCalendarMeetings.help = 'integration/task/msgraph-calendar-meetings';
|
|
169
|
+
integration_base_1.TaskRegistry.registerTaskHandler('msgraph-calendar-meetings', MsGraphCalendarMeetings);
|
|
170
|
+
//# sourceMappingURL=msgraph-calendar-meetings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msgraph-calendar-meetings.js","sourceRoot":"","sources":["../../../server/engine/task/msgraph-calendar-meetings.ts"],"names":[],"mappings":"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","sourcesContent":["import { ConnectionManager, TaskRegistry } from '@things-factory/integration-base'\n\n/*\n * The meetings on people's calendars over some days - for a scenario that brings them into the domain's calendar\n * (@things-factory/calendar), where the weekly report reads them (Office ADR-0191). Read with the application permission Calendars.ReadBasic.All (no bodies),\n * which the tenant narrows to the people's mailboxes (an Exchange application access policy).\n *\n * A Teams meeting that has ended carries how long each person was actually in it - the meeting attendance report, read\n * from its organizer with OnlineMeetingArtifact.Read.All (a Teams application access policy names the organizers). A\n * meeting whose report cannot be read is still answered, with no attendance.\n */\n\nexport interface MeetingAttendee {\n email: string\n name: string | null\n /** The invitation's answer: accepted, tentativelyAccepted, declined, notResponded, organizer or none. */\n response: string | null\n}\n\nexport interface MeetingAttendance {\n email: string\n seconds: number\n}\n\nexport interface CalendarMeeting {\n /** Whose calendar it was read from. */\n mailbox: string\n /** The occurrence's id across calendars (iCalUId) - one per occurrence of a series too. */\n id: string\n subject: string\n start: string\n end: string\n organizer: string | null\n attendees: MeetingAttendee[]\n online: boolean\n location: string | null\n /** The mailbox owner's own answer to it. */\n response: string | null\n /** How long each person was in it - a Teams meeting that has ended, when its report could be read; else null. */\n attendance: MeetingAttendance[] | null\n /** Cancelled there - handed on so a calendar that brought it in cancels it too. */\n cancelled: boolean\n}\n\nconst address = (one: any) => String(one?.emailAddress?.address ?? '').toLowerCase()\nconst utc = (one: any) => (one?.dateTime ? new Date(`${String(one.dateTime).replace(/Z$/, '')}Z`).toISOString() : '')\n\n/** What a Graph event says, as the keeper of meetings reads it - null for a private one or one with nobody else in it. */\nexport function meetingOf(mailbox: string, raw: any): CalendarMeeting | null {\n if (!raw?.iCalUId) return null\n if (['private', 'confidential'].includes(String(raw.sensitivity ?? '').toLowerCase())) return null\n const attendees: MeetingAttendee[] = (raw.attendees ?? [])\n .map((one: any) => ({ email: address(one), name: one?.emailAddress?.name ?? null, response: one?.status?.response ?? null }))\n .filter((one: MeetingAttendee) => one.email)\n const organizer = address(raw.organizer) || null\n /* A meeting has someone other than the mailbox's owner in it. */\n const others = new Set([organizer, ...attendees.map(one => one.email)].filter(one => one && one !== mailbox))\n if (!others.size) return null\n return {\n mailbox,\n id: raw.iCalUId,\n subject: String(raw.subject ?? ''),\n start: utc(raw.start),\n end: utc(raw.end),\n organizer,\n attendees,\n online: !!raw.isOnlineMeeting && String(raw.onlineMeetingProvider ?? '') === 'teamsForBusiness',\n location: raw.location?.displayName || null,\n response: raw.responseStatus?.response ?? null,\n attendance: null,\n cancelled: !!raw.isCancelled\n }\n}\n\nconst FIELDS = ['iCalUId', 'subject', 'start', 'end', 'organizer', 'attendees', 'isOnlineMeeting', 'onlineMeetingProvider', 'onlineMeeting', 'sensitivity', 'isCancelled', 'location', 'responseStatus']\n\nasync function pages(client: any, request: any) {\n const out: any[] = []\n let page = await request.get()\n for (;;) {\n out.push(...(page?.value ?? []))\n const next = page?.['@odata.nextLink']\n if (!next) return out\n page = await client.api(next).header('Prefer', 'outlook.timezone=\"UTC\"').get()\n }\n}\n\n/**\n * How long each person was in a Teams meeting: its organizer's online meeting found by the join address, the report\n * of the occurrence that began within the event, and that report's records. Null when any of it cannot be read.\n */\nexport async function attendanceOf(client: any, organizer: string, joinUrl: string, start: string, end: string): Promise<MeetingAttendance[] | null> {\n try {\n const user = `/users/${encodeURIComponent(organizer)}`\n const found = await client.api(`${user}/onlineMeetings`).filter(`JoinWebUrl eq '${joinUrl.replace(/'/g, \"''\")}'`).get()\n const meeting = found?.value?.[0]\n if (!meeting?.id) return null\n const reports: any[] = (await client.api(`${user}/onlineMeetings/${meeting.id}/attendanceReports`).get())?.value ?? []\n /* A series has a report per occurrence; this occurrence's began between an hour before it and its end. */\n const from = Date.parse(start) - 3600000\n const until = Date.parse(end)\n const report = reports.find(one => {\n const began = Date.parse(one.meetingStartDateTime)\n return began >= from && began <= until\n })\n if (!report) return null\n const records: any[] = await pages(client, client.api(`${user}/onlineMeetings/${meeting.id}/attendanceReports/${report.id}/attendanceRecords`))\n const seconds = new Map<string, number>()\n for (const one of records) {\n const email = String(one.emailAddress ?? '').toLowerCase()\n if (email) seconds.set(email, (seconds.get(email) ?? 0) + (Number(one.totalAttendanceInSeconds) || 0))\n }\n return [...seconds].map(([email, total]) => ({ email, seconds: total }))\n } catch {\n return null\n }\n}\n\n/**\n * What one reading found: the window it read, the mailboxes it could read and those it could not, and the meetings.\n * A calendar that brings them in may take a meeting of the window it no longer finds in a mailbox read as removed there.\n */\nexport interface CalendarReading {\n from: string\n to: string\n mailboxes: string[]\n failed: { mailbox: string; reason: string }[]\n meetings: CalendarMeeting[]\n}\n\n/**\n * The meetings of each mailbox between `from` and `to`, each Teams meeting that has ended with its attendance. A mailbox\n * that cannot be read - one with no calendar, a person the access policy leaves out - is named in `failed` and the rest\n * are read; when none can be read the reading fails, for then it is the permission, not a person.\n */\nexport async function readCalendarMeetings(client: any, mailboxes: string[], from: string, to: string, now = Date.now()): Promise<CalendarReading> {\n const reading: CalendarReading = { from: new Date(from).toISOString(), to: new Date(to).toISOString(), mailboxes: [], failed: [], meetings: [] }\n const attendance = new Map<string, Promise<MeetingAttendance[] | null>>()\n for (const raw of mailboxes) {\n const mailbox = String(raw ?? '').trim().toLowerCase()\n if (!/^[^@\\s]+@[^@\\s]+$/.test(mailbox)) throw new Error(`'${raw}' is not a mailbox address`)\n let events: any[]\n try {\n events = await pages(\n client,\n client\n .api(`/users/${encodeURIComponent(mailbox)}/calendarView`)\n .header('Prefer', 'outlook.timezone=\"UTC\"')\n .query({ startDateTime: reading.from, endDateTime: reading.to })\n .select(FIELDS)\n .top(100)\n )\n } catch (error: any) {\n const code = error?.code ? ` (${error.code})` : ''\n reading.failed.push({ mailbox, reason: `${error?.message ?? error}${code}` })\n continue\n }\n reading.mailboxes.push(mailbox)\n for (const event of events) {\n const meeting = meetingOf(mailbox, event)\n if (!meeting) continue\n const joinUrl = event.onlineMeeting?.joinUrl\n if (!meeting.cancelled && meeting.online && joinUrl && meeting.organizer && Date.parse(meeting.end) < now) {\n if (!attendance.has(meeting.id)) attendance.set(meeting.id, attendanceOf(client, meeting.organizer, joinUrl, meeting.start, meeting.end))\n meeting.attendance = await attendance.get(meeting.id)!\n }\n reading.meetings.push(meeting)\n }\n }\n if (mailboxes.length && !reading.mailboxes.length) {\n const first = reading.failed[0]\n throw new Error(`no calendar could be read - ${first.mailbox}: ${first.reason} - needs the application permission Calendars.ReadBasic.All with admin consent, allowed for these mailboxes`)\n }\n return reading\n}\n\nconst read = (data: any, accessor: any) => (accessor ? String(accessor).split('.').reduce((value: any, key: string) => value?.[key], data) : undefined)\n\n/**\n * The mailboxes named by a list: addresses, or records that carry one - an employee list read in an earlier step\n * (`{ active, user: { email } }`) gives the address of each active one with a user.\n */\nexport function mailboxesOf(listed: unknown): string[] {\n const items = Array.isArray(listed) ? listed : String(listed ?? '').split(/[,\\s]+/)\n const out = new Set<string>()\n for (const one of items) {\n if (one && typeof one === 'object' && (one as any).active === false) continue\n const address = typeof one === 'string' ? one : (one as any)?.email ?? (one as any)?.user?.email\n if (address && String(address).trim()) out.add(String(address).trim().toLowerCase())\n }\n return [...out]\n}\n\nasync function MsGraphCalendarMeetings(step, context) {\n const { connection: connectionName, params } = step\n const { domain, data } = context\n const client = await ConnectionManager.getConnectionInstanceByName(domain, connectionName)\n if (!client) throw new Error(`no connection '${connectionName}'`)\n const { mailboxes, mailboxesAccessor, daysBefore, daysAfter } = params || {}\n /* The people: given, or read off an earlier step (the keeper's list of mailboxes). */\n const people = mailboxesOf(read(data, mailboxesAccessor) ?? mailboxes ?? [])\n const now = Date.now()\n const from = new Date(now - (Number(daysBefore) || 14) * 86400000).toISOString()\n const to = new Date(now + (Number(daysAfter) || 14) * 86400000).toISOString()\n return { data: await readCalendarMeetings(client, people, from, to, now) }\n}\n\nMsGraphCalendarMeetings.parameterSpec = [\n { type: 'string', name: 'mailboxes', label: 'mailboxes' },\n { type: 'string', name: 'mailboxesAccessor', label: 'mailboxes-accessor' },\n { type: 'number', name: 'daysBefore', label: 'days-before' },\n { type: 'number', name: 'daysAfter', label: 'days-after' }\n]\nMsGraphCalendarMeetings.help = 'integration/task/msgraph-calendar-meetings'\n\nTaskRegistry.registerTaskHandler('msgraph-calendar-meetings', MsGraphCalendarMeetings)\n"]}
|