diff --git a/scripts/fetch-meeting-artifacts.js b/scripts/fetch-meeting-artifacts.js index e22e181..35c8bd1 100644 --- a/scripts/fetch-meeting-artifacts.js +++ b/scripts/fetch-meeting-artifacts.js @@ -99,8 +99,12 @@ async function main() { process.exit(2); } const env = loadEnv(); - const userId = opts.user || env.AZURE_USER_EMAIL; + 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'); diff --git a/scripts/lib/graph-meetings.js b/scripts/lib/graph-meetings.js index 56599cf..142fcdc 100644 --- a/scripts/lib/graph-meetings.js +++ b/scripts/lib/graph-meetings.js @@ -62,14 +62,37 @@ async function fetchTranscriptVtt(client, userId, meetingId) { new Date(b.createdDateTime) - new Date(a.createdDateTime) )[0]; const contentPath = `/users/${userId}/onlineMeetings/${meetingId}/transcripts/${latest.id}/content`; - return await safeGet(client, contentPath); + // Transcript content endpoint returns text/vtt; default Accept */* is rejected. + try { + let req = client.api(contentPath).query({ $format: 'text/vtt' }); + if (typeof req.header === 'function') req = req.header('Accept', 'text/vtt'); + if (typeof req.responseType === 'function') req = req.responseType('text'); + const resp = await req.get(); + if (typeof resp === 'string') return resp; + if (resp && typeof resp.text === 'function') return await resp.text(); + return resp; + } catch (err) { + if (err.statusCode === 404 || err.statusCode === 403) return null; + throw err; + } } async function fetchAiInsights(client, userId, meetingId) { - const path = `/users/${userId}/onlineMeetings/${meetingId}/aiInsights`; - const resp = await safeGet(client, path, { version: 'beta' }); - const items = resp?.value || []; - return items[0] || null; + // Copilot recap moved under /copilot/... (beta) ca. 2025-2026. + // List call returns metadata only; detail call required for meetingNotes/actionItems. + const listPath = `/copilot/users/${userId}/onlineMeetings/${meetingId}/aiInsights`; + const list = await safeGet(client, listPath, { version: 'beta' }); + const items = list?.value || []; + if (items.length === 0) return null; + const details = []; + for (const item of items) { + const detail = await safeGet(client, `${listPath}/${item.id}`, { version: 'beta' }); + if (detail) details.push(detail); + } + if (details.length === 0) return null; + // Return single object if one insight, otherwise wrap. + if (details.length === 1) return details[0]; + return { id: details[0].id, insights: details }; } async function fetchRecordingUrl(client, userId, meetingId) { diff --git a/scripts/test/graph-meetings.test.js b/scripts/test/graph-meetings.test.js index a6a3bc2..d1ae82d 100644 --- a/scripts/test/graph-meetings.test.js +++ b/scripts/test/graph-meetings.test.js @@ -71,8 +71,11 @@ describe('fetchTranscriptVtt', () => { describe('fetchAiInsights', () => { it('returns insights when available', async () => { const client = fakeClient({ - "/users/u/onlineMeetings/M1/aiInsights": { - value: [{ id: 'I1', actionItems: [], meetingNotes: [], mentions: [] }] + "/copilot/users/u/onlineMeetings/M1/aiInsights": { + value: [{ id: 'I1' }] + }, + "/copilot/users/u/onlineMeetings/M1/aiInsights/I1": { + id: 'I1', actionItems: [], meetingNotes: [], mentions: [] } }); const out = await fetchAiInsights(client, 'u', 'M1');