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(//gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/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 };