feat(scripts): add graph meetings helper with safe fallbacks
This commit is contained in:
parent
f6391b0258
commit
95cb27e53a
|
|
@ -0,0 +1,92 @@
|
|||
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;
|
||||
}
|
||||
|
||||
async function buildGraphClient(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),
|
||||
defaultVersion: 'v1.0'
|
||||
});
|
||||
}
|
||||
|
||||
async function safeGet(client, path, options = {}) {
|
||||
try {
|
||||
let req = client.api(path);
|
||||
if (options.version && typeof req.version === 'function') req = req.version(options.version);
|
||||
if (options.query && typeof req.query === 'function') req = req.query(options.query);
|
||||
return await req.get();
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404 || err.statusCode === 403) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveOnlineMeeting(client, userId, joinWebUrl) {
|
||||
const path = `/users/${userId}/onlineMeetings`;
|
||||
const resp = await safeGet(client, path, {
|
||||
query: { $filter: `JoinWebUrl eq '${joinWebUrl}'` }
|
||||
});
|
||||
const list = resp?.value || [];
|
||||
return list[0] || null;
|
||||
}
|
||||
|
||||
async function fetchTranscriptVtt(client, userId, meetingId) {
|
||||
const list = await safeGet(client, `/users/${userId}/onlineMeetings/${meetingId}/transcripts`);
|
||||
const items = list?.value || [];
|
||||
if (items.length === 0) return null;
|
||||
const latest = [...items].sort((a, b) =>
|
||||
new Date(b.createdDateTime) - new Date(a.createdDateTime)
|
||||
)[0];
|
||||
const contentPath = `/users/${userId}/onlineMeetings/${meetingId}/transcripts/${latest.id}/content`;
|
||||
return await safeGet(client, contentPath);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function fetchRecordingUrl(client, userId, meetingId) {
|
||||
const list = await safeGet(client, `/users/${userId}/onlineMeetings/${meetingId}/recordings`);
|
||||
const items = list?.value || [];
|
||||
if (items.length === 0) return null;
|
||||
const latest = [...items].sort((a, b) =>
|
||||
new Date(b.createdDateTime) - new Date(a.createdDateTime)
|
||||
)[0];
|
||||
return latest.recordingContentUrl || latest.contentCorrelationId || null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadEnv,
|
||||
buildGraphClient,
|
||||
resolveOnlineMeeting,
|
||||
fetchTranscriptVtt,
|
||||
fetchAiInsights,
|
||||
fetchRecordingUrl
|
||||
};
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
resolveOnlineMeeting,
|
||||
fetchTranscriptVtt,
|
||||
fetchAiInsights,
|
||||
fetchRecordingUrl
|
||||
} = require('../lib/graph-meetings.js');
|
||||
|
||||
function fakeClient(routes) {
|
||||
return {
|
||||
api(path) {
|
||||
const handler = routes[path];
|
||||
const builder = {
|
||||
query() { return builder; },
|
||||
select() { return builder; },
|
||||
version() { return builder; },
|
||||
get: async () => {
|
||||
if (handler === undefined) throw Object.assign(new Error('Not found'), { statusCode: 404 });
|
||||
if (typeof handler === 'function') return handler();
|
||||
return handler;
|
||||
}
|
||||
};
|
||||
return builder;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolveOnlineMeeting', () => {
|
||||
it('resolves meeting by joinWebUrl', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings": { value: [{ id: 'M1', joinWebUrl: 'https://teams/j' }] }
|
||||
});
|
||||
const m = await resolveOnlineMeeting(client, 'u', 'https://teams/j');
|
||||
assert.equal(m.id, 'M1');
|
||||
});
|
||||
|
||||
it('returns null when no meeting matches', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings": { value: [] }
|
||||
});
|
||||
const m = await resolveOnlineMeeting(client, 'u', 'https://teams/j');
|
||||
assert.equal(m, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTranscriptVtt', () => {
|
||||
it('returns latest transcript content as string', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings/M1/transcripts": {
|
||||
value: [
|
||||
{ id: 'T1', createdDateTime: '2026-05-06T09:00:00Z' },
|
||||
{ id: 'T2', createdDateTime: '2026-05-06T10:00:00Z' }
|
||||
]
|
||||
},
|
||||
"/users/u/onlineMeetings/M1/transcripts/T2/content": 'WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n<v X>Y</v>\n'
|
||||
});
|
||||
const vtt = await fetchTranscriptVtt(client, 'u', 'M1');
|
||||
assert.ok(vtt.startsWith('WEBVTT'));
|
||||
});
|
||||
|
||||
it('returns null when no transcripts exist', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings/M1/transcripts": { value: [] }
|
||||
});
|
||||
const vtt = await fetchTranscriptVtt(client, 'u', 'M1');
|
||||
assert.equal(vtt, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAiInsights', () => {
|
||||
it('returns insights when available', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings/M1/aiInsights": {
|
||||
value: [{ id: 'I1', actionItems: [], meetingNotes: [], mentions: [] }]
|
||||
}
|
||||
});
|
||||
const out = await fetchAiInsights(client, 'u', 'M1');
|
||||
assert.ok(out);
|
||||
assert.equal(out.id, 'I1');
|
||||
});
|
||||
|
||||
it('returns null on 404', async () => {
|
||||
const client = fakeClient({});
|
||||
const out = await fetchAiInsights(client, 'u', 'M1');
|
||||
assert.equal(out, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchRecordingUrl', () => {
|
||||
it('returns latest recording url', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings/M1/recordings": {
|
||||
value: [{ id: 'R1', recordingContentUrl: 'https://teams/play/R1', createdDateTime: '2026-05-06T11:00:00Z' }]
|
||||
}
|
||||
});
|
||||
const out = await fetchRecordingUrl(client, 'u', 'M1');
|
||||
assert.equal(out, 'https://teams/play/R1');
|
||||
});
|
||||
|
||||
it('returns null when no recording', async () => {
|
||||
const client = fakeClient({
|
||||
"/users/u/onlineMeetings/M1/recordings": { value: [] }
|
||||
});
|
||||
const out = await fetchRecordingUrl(client, 'u', 'M1');
|
||||
assert.equal(out, null);
|
||||
});
|
||||
|
||||
it('returns null on permission denied', async () => {
|
||||
const client = fakeClient({});
|
||||
const out = await fetchRecordingUrl(client, 'u', 'M1');
|
||||
assert.equal(out, null);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue