brain/scripts/lib/o365-calendar.js

132 lines
3.7 KiB
JavaScript

const { ConfidentialClientApplication } = require('@azure/msal-node');
const { Client } = require('@microsoft/microsoft-graph-client');
const { readFileSync } = require('node:fs');
const { resolve } = require('node:path');
function loadEnv() {
const envPath = resolve(__dirname, '..', '.env');
const content = readFileSync(envPath, 'utf-8');
const vars = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [key, ...rest] = trimmed.split('=');
vars[key.trim()] = rest.join('=').trim();
}
return vars;
}
function stripHtml(html) {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/p>/gi, '\n')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function isRecurring(event) {
return Boolean(event.seriesMasterId);
}
function parseEventToMeeting(event) {
const startDate = event.start.dateTime.split('T');
const endDate = event.end.dateTime.split('T');
return {
id: event.id,
title: event.subject,
date: startDate[0],
start: startDate[1].substring(0, 5),
end: endDate[1].substring(0, 5),
bodyText: event.body?.contentType === 'html'
? stripHtml(event.body.content)
: (event.body?.content || '').trim(),
attendees: (event.attendees || []).map(a => ({
name: a.emailAddress.name,
email: a.emailAddress.address.toLowerCase()
})),
isRecurring: isRecurring(event)
};
}
function formatEventChoice(meeting) {
return `📅 ${meeting.date} ${meeting.start}-${meeting.end} ${meeting.title}`;
}
async function buildAuthenticatedClient() {
const env = loadEnv();
const cca = new ConfidentialClientApplication({
auth: {
clientId: env.AZURE_CLIENT_ID,
clientSecret: env.AZURE_CLIENT_SECRET,
authority: `https://login.microsoftonline.com/${env.AZURE_TENANT_ID}`
}
});
const tokenResponse = await cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default']
});
return Client.init({
authProvider: (done) => done(null, tokenResponse.accessToken)
});
}
async function getCalendarEvents(daysAhead = 7) {
const env = loadEnv();
const client = await buildAuthenticatedClient();
const now = new Date();
const future = new Date(now);
future.setDate(future.getDate() + daysAhead);
const response = await client
.api(`/users/${env.AZURE_USER_EMAIL}/calendarView`)
.query({
startDateTime: now.toISOString(),
endDateTime: future.toISOString()
})
.select('id,subject,start,end,body,attendees,seriesMasterId')
.orderby('start/dateTime')
.top(500)
.get();
return (response.value || []).map(parseEventToMeeting);
}
function extractJoinUrlFromBody(body) {
if (!body) return null;
const m = body.match(/https:\/\/teams\.microsoft\.com\/l\/meetup-join\/[^\s"'<>]+/);
return m ? m[0] : null;
}
async function getEventById(eventId) {
if (typeof eventId !== 'string' || !eventId.trim()) {
throw new TypeError('eventId must be a non-empty string');
}
try {
const env = loadEnv();
const client = await buildAuthenticatedClient();
const event = await client
.api(`/users/${env.AZURE_USER_EMAIL}/events/${eventId}`)
.select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting')
.get();
return event;
} catch (cause) {
throw new Error(`Failed to fetch event ${eventId}: ${cause.message}`, { cause });
}
}
module.exports = {
isRecurring, parseEventToMeeting, formatEventChoice,
getCalendarEvents, getEventById, extractJoinUrlFromBody
};