// scripts/fetch-meeting-artifacts.js const { writeFileSync } = require('node:fs'); const { loadEnv, buildGraphClient, resolveOnlineMeeting, fetchTranscriptVtt, fetchAiInsights, fetchRecordingUrl } = require('./lib/graph-meetings.js'); const { extractJoinUrlFromBody } = require('./lib/o365-calendar.js'); const { parseVtt, formatTranscript } = require('./lib/vtt-parser.js'); function parseArgs(argv) { const out = {}; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === '--o365-id') out.o365Id = argv[++i]; else if (a === '--join-url') out.joinUrl = argv[++i]; else if (a === '--out') out.out = argv[++i]; else if (a === '--user') out.user = argv[++i]; } return out; } async function runFetch(client, userId, opts) { const warnings = []; let event = null; let joinUrl = opts.joinUrl || null; let seriesMasterId = null; if (opts.o365Id) { event = await client.api(`/users/${userId}/events/${opts.o365Id}`) .select('id,subject,start,end,body,attendees,seriesMasterId,onlineMeeting') .get(); seriesMasterId = event.seriesMasterId || null; if (!joinUrl) { joinUrl = event.onlineMeeting?.joinUrl || extractJoinUrlFromBody(event.body?.content || ''); } } if (!joinUrl) { warnings.push('no teams join url on event'); return { meeting: event ? eventSummary(event) : null, transcript: null, recap: null, recordingUrl: null, warnings }; } const om = await resolveOnlineMeeting(client, userId, joinUrl); if (!om) { warnings.push(`onlineMeeting not found for joinUrl=${joinUrl}`); return { meeting: event ? eventSummary(event) : null, transcript: null, recap: null, recordingUrl: null, warnings }; } const [vtt, recap, recordingUrl] = await Promise.all([ fetchTranscriptVtt(client, userId, om.id).catch(e => { warnings.push(`transcript: ${e.message}`); return null; }), fetchAiInsights(client, userId, om.id).catch(e => { warnings.push(`aiInsights: ${e.message}`); return null; }), fetchRecordingUrl(client, userId, om.id).catch(e => { warnings.push(`recording: ${e.message}`); return null; }) ]); let transcript = null; if (vtt) { const cues = parseVtt(vtt, { mergeConsecutive: true }); transcript = formatTranscript(cues); } return { meeting: event ? { ...eventSummary(event), onlineMeetingId: om.id, seriesMasterId } : { onlineMeetingId: om.id }, transcript, recap, recordingUrl, warnings }; } function eventSummary(event) { return { id: event.id, subject: event.subject, start: event.start?.dateTime, end: event.end?.dateTime, seriesMasterId: event.seriesMasterId || null, attendees: (event.attendees || []).map(a => ({ name: a.emailAddress?.name, email: (a.emailAddress?.address || '').toLowerCase() })) }; } async function main() { const opts = parseArgs(process.argv); if (!opts.o365Id && !opts.joinUrl) { console.error('Usage: node fetch-meeting-artifacts.js --o365-id [--out file.json]'); process.exit(2); } const env = loadEnv(); const upn = opts.user || env.AZURE_USER_EMAIL; const client = await buildGraphClient(env); // Resolve UPN -> Object ID. OnlineMeetings API + Application Access Policy // match users by OID; UPN routing returns 404 even with correct policy. const me = await client.api('/users/' + upn).select('id').get(); const userId = me.id; const result = await runFetch(client, userId, opts); const json = JSON.stringify(result, null, 2); if (opts.out) writeFileSync(opts.out, json, 'utf-8'); else process.stdout.write(json + '\n'); } if (require.main === module) { main().catch(err => { console.error(err.stack || err.message); process.exit(1); }); } module.exports = { runFetch, parseArgs };