feat(scripts): add fetch-meeting-artifacts orchestrator cli
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51613ffe12
commit
15499cf190
|
|
@ -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 <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 };
|
||||
|
|
@ -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: '<a href="https://teams.microsoft.com/l/meetup-join/19%3aXYZ/0">Join</a>' }
|
||||
},
|
||||
[`/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\n<v Christian Kauer>Hallo.</v>\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: '<a href="https://teams.microsoft.com/l/meetup-join/19%3aabc/0">J</a>' }
|
||||
},
|
||||
[`/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)));
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue