From 15499cf19044063de4b73165ff0c6a4568f84f19 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Wed, 6 May 2026 21:06:01 +0200 Subject: [PATCH] feat(scripts): add fetch-meeting-artifacts orchestrator cli Co-Authored-By: Claude Sonnet 4.6 --- scripts/fetch-meeting-artifacts.js | 114 +++++++++++++++++++ scripts/test/fetch-meeting-artifacts.test.js | 88 ++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 scripts/fetch-meeting-artifacts.js create mode 100644 scripts/test/fetch-meeting-artifacts.test.js diff --git a/scripts/fetch-meeting-artifacts.js b/scripts/fetch-meeting-artifacts.js new file mode 100644 index 0000000..e22e181 --- /dev/null +++ b/scripts/fetch-meeting-artifacts.js @@ -0,0 +1,114 @@ +// 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 userId = opts.user || env.AZURE_USER_EMAIL; + const client = await buildGraphClient(env); + 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 }; diff --git a/scripts/test/fetch-meeting-artifacts.test.js b/scripts/test/fetch-meeting-artifacts.test.js new file mode 100644 index 0000000..033db20 --- /dev/null +++ b/scripts/test/fetch-meeting-artifacts.test.js @@ -0,0 +1,88 @@ +// scripts/test/fetch-meeting-artifacts.test.js +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const { runFetch } = require('../fetch-meeting-artifacts.js'); + +function makeClient(map) { + return { + api(path) { + const h = map[path]; + const builder = { + query() { return builder; }, + select() { return builder; }, + version() { return builder; }, + async get() { + if (h === undefined) throw Object.assign(new Error('nf'), { statusCode: 404 }); + return typeof h === 'function' ? h() : h; + } + }; + return builder; + } + }; +} + +describe('runFetch', () => { + it('returns full artifact bundle for happy path', async () => { + const userId = 'u@krah.de'; + const client = makeClient({ + [`/users/${userId}/events/E1`]: { + id: 'E1', subject: 'Jour Fixe IT Team', seriesMasterId: 'S1', + start: { dateTime: '2026-05-06T09:00:00.0000000', timeZone: 'UTC' }, + end: { dateTime: '2026-05-06T10:00:00.0000000', timeZone: 'UTC' }, + attendees: [{ emailAddress: { name: 'Christian Kauer', address: 'c.kauer@krah-gruppe.de' } }], + body: { contentType: 'html', content: 'Join' } + }, + [`/users/${userId}/onlineMeetings`]: { value: [{ id: 'M1', joinWebUrl: 'https://teams.microsoft.com/l/meetup-join/19%3aXYZ/0' }] }, + [`/users/${userId}/onlineMeetings/M1/transcripts`]: { value: [{ id: 'T1', createdDateTime: '2026-05-06T10:01:00Z' }] }, + [`/users/${userId}/onlineMeetings/M1/transcripts/T1/content`]: 'WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nHallo.\n', + [`/users/${userId}/onlineMeetings/M1/aiInsights`]: { value: [{ id: 'I1', meetingNotes: [{ title: 'Topic', text: 'Notiz' }] }] }, + [`/users/${userId}/onlineMeetings/M1/recordings`]: { value: [{ id: 'R1', recordingContentUrl: 'https://teams/play/R1', createdDateTime: '2026-05-06T10:30:00Z' }] } + }); + + const result = await runFetch(client, userId, { o365Id: 'E1' }); + + assert.equal(result.meeting.id, 'E1'); + assert.equal(result.meeting.seriesMasterId, 'S1'); + assert.equal(result.meeting.onlineMeetingId, 'M1'); + assert.ok(result.transcript.includes('Christian Kauer: Hallo.')); + assert.equal(result.recap.id, 'I1'); + assert.equal(result.recordingUrl, 'https://teams/play/R1'); + }); + + it('handles missing transcript gracefully', async () => { + const userId = 'u@krah.de'; + const client = makeClient({ + [`/users/${userId}/events/E1`]: { + id: 'E1', subject: 'X', seriesMasterId: null, + start: { dateTime: '2026-05-06T09:00:00.0000000' }, + end: { dateTime: '2026-05-06T10:00:00.0000000' }, + attendees: [], + body: { contentType: 'html', content: 'J' } + }, + [`/users/${userId}/onlineMeetings`]: { value: [{ id: 'M1', joinWebUrl: 'https://teams.microsoft.com/l/meetup-join/19%3aabc/0' }] }, + [`/users/${userId}/onlineMeetings/M1/transcripts`]: { value: [] }, + [`/users/${userId}/onlineMeetings/M1/recordings`]: { value: [] } + }); + const result = await runFetch(client, userId, { o365Id: 'E1' }); + assert.equal(result.transcript, null); + assert.equal(result.recap, null); + assert.equal(result.recordingUrl, null); + }); + + it('returns error info when event has no teams join url', async () => { + const userId = 'u@krah.de'; + const client = makeClient({ + [`/users/${userId}/events/E1`]: { + id: 'E1', subject: 'X', seriesMasterId: null, + start: { dateTime: '2026-05-06T09:00:00.0000000' }, + end: { dateTime: '2026-05-06T10:00:00.0000000' }, + attendees: [], + body: { contentType: 'text', content: 'no join url here' } + } + }); + const result = await runFetch(client, userId, { o365Id: 'E1' }); + assert.equal(result.transcript, null); + assert.equal(result.recap, null); + assert.ok(result.warnings.some(w => /join url/i.test(w))); + }); +});