mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:20:44 +00:00
feat(google-meet): add artifacts and attendance commands
This commit is contained in:
@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/Google Meet: add a bundled participant plugin with personal Google auth, explicit meeting URL joins, Chrome and Twilio transports, and realtime voice support. (#70765) Thanks @steipete.
|
||||
- Plugins/Google Meet: default Chrome realtime sessions to OpenAI plus SoX `rec`/`play` audio bridge commands, so the usual setup only needs the plugin enabled and `OPENAI_API_KEY`. Thanks @steipete.
|
||||
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
|
||||
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts, smart notes, and participant sessions. Thanks @steipete.
|
||||
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
|
||||
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.
|
||||
- Plugins/Google Meet: add `googlemeet doctor` and a `recover_current_tab`/`recover-tab` flow so agents can inspect an already-open Meet tab and report the blocker without opening another window. Thanks @steipete.
|
||||
|
||||
@@ -480,6 +480,27 @@ Run preflight before media work:
|
||||
openclaw googlemeet preflight --meeting https://meet.google.com/abc-defg-hij
|
||||
```
|
||||
|
||||
List meeting artifacts and attendance after Meet has created conference records:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet artifacts --meeting https://meet.google.com/abc-defg-hij
|
||||
openclaw googlemeet attendance --meeting https://meet.google.com/abc-defg-hij
|
||||
```
|
||||
|
||||
If you already know the conference record id, address it directly:
|
||||
|
||||
```bash
|
||||
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --json
|
||||
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
|
||||
```
|
||||
|
||||
`artifacts` returns conference record metadata plus participant, recording,
|
||||
transcript, and smart-note resource metadata when Google exposes it for the
|
||||
meeting. `attendance` expands participants into participant-session rows with
|
||||
join/leave timestamps. These commands use the Meet REST API only; transcript or
|
||||
smart-note document body download is intentionally out of scope because that
|
||||
requires separate Google Docs/Drive access.
|
||||
|
||||
Create a fresh Meet space:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -9,6 +9,8 @@ import { resolveGoogleMeetConfig, resolveGoogleMeetConfigWithEnv } from "./src/c
|
||||
import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
createGoogleMeetSpace,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchGoogleMeetSpace,
|
||||
normalizeGoogleMeetSpaceName,
|
||||
} from "./src/meet.js";
|
||||
@@ -64,6 +66,112 @@ function setup(
|
||||
return setupGoogleMeetPlugin(plugin, config, options);
|
||||
}
|
||||
|
||||
function jsonResponse(value: unknown): Response {
|
||||
return new Response(JSON.stringify(value), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function requestUrl(input: RequestInfo | URL): URL {
|
||||
if (typeof input === "string") {
|
||||
return new URL(input);
|
||||
}
|
||||
if (input instanceof URL) {
|
||||
return input;
|
||||
}
|
||||
return new URL(input.url);
|
||||
}
|
||||
|
||||
function stubMeetArtifactsApi() {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = requestUrl(input);
|
||||
if (url.pathname === "/v2/spaces/abc-defg-hij") {
|
||||
return jsonResponse({
|
||||
name: "spaces/abc-defg-hij",
|
||||
meetingCode: "abc-defg-hij",
|
||||
meetingUri: "https://meet.google.com/abc-defg-hij",
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords") {
|
||||
return jsonResponse({
|
||||
conferenceRecords: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1",
|
||||
space: "spaces/abc-defg-hij",
|
||||
startTime: "2026-04-25T10:00:00Z",
|
||||
endTime: "2026-04-25T10:30:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1") {
|
||||
return jsonResponse({
|
||||
name: "conferenceRecords/rec-1",
|
||||
space: "spaces/abc-defg-hij",
|
||||
startTime: "2026-04-25T10:00:00Z",
|
||||
endTime: "2026-04-25T10:30:00Z",
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1/participants") {
|
||||
return jsonResponse({
|
||||
participants: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/participants/p1",
|
||||
earliestStartTime: "2026-04-25T10:00:00Z",
|
||||
latestEndTime: "2026-04-25T10:30:00Z",
|
||||
signedinUser: { user: "users/alice", displayName: "Alice" },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions") {
|
||||
return jsonResponse({
|
||||
participantSessions: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
startTime: "2026-04-25T10:00:00Z",
|
||||
endTime: "2026-04-25T10:30:00Z",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1/recordings") {
|
||||
return jsonResponse({
|
||||
recordings: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/recordings/r1",
|
||||
driveDestination: { file: "drive/file-1" },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts") {
|
||||
return jsonResponse({
|
||||
transcripts: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/transcripts/t1",
|
||||
docsDestination: { document: "docs/doc-1" },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes") {
|
||||
return jsonResponse({
|
||||
smartNotes: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/smartNotes/sn1",
|
||||
docsDestination: { document: "docs/doc-2" },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return new Response(`unexpected ${url.pathname}`, { status: 404 });
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
type TestBridgeProcess = {
|
||||
stdin?: { write(chunk: unknown): unknown } | null;
|
||||
stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null;
|
||||
@@ -218,6 +326,8 @@ describe("google-meet plugin", () => {
|
||||
"setup_status",
|
||||
"resolve_space",
|
||||
"preflight",
|
||||
"artifacts",
|
||||
"attendance",
|
||||
"recover_current_tab",
|
||||
"leave",
|
||||
"speak",
|
||||
@@ -310,6 +420,82 @@ describe("google-meet plugin", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("lists Meet artifact metadata for conference records", async () => {
|
||||
const fetchMock = stubMeetArtifactsApi();
|
||||
|
||||
await expect(
|
||||
fetchGoogleMeetArtifacts({
|
||||
accessToken: "token",
|
||||
meeting: "abc-defg-hij",
|
||||
pageSize: 2,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
input: "abc-defg-hij",
|
||||
space: { name: "spaces/abc-defg-hij" },
|
||||
conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
|
||||
artifacts: [
|
||||
{
|
||||
conferenceRecord: { name: "conferenceRecords/rec-1" },
|
||||
participants: [{ name: "conferenceRecords/rec-1/participants/p1" }],
|
||||
recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
|
||||
transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
|
||||
smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const listCall = fetchMock.mock.calls.find(([input]) => {
|
||||
const url = requestUrl(input);
|
||||
return url.pathname === "/v2/conferenceRecords";
|
||||
});
|
||||
if (!listCall) {
|
||||
throw new Error("Expected conferenceRecords.list fetch call");
|
||||
}
|
||||
const listUrl = requestUrl(listCall[0]);
|
||||
expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"');
|
||||
expect(listUrl.searchParams.get("pageSize")).toBe("2");
|
||||
expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://meet.googleapis.com/v2/conferenceRecords/rec-1/smartNotes?pageSize=2",
|
||||
auditContext: "google-meet.conferenceRecords.smartNotes.list",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lists Meet attendance rows with participant sessions", async () => {
|
||||
const fetchMock = stubMeetArtifactsApi();
|
||||
|
||||
await expect(
|
||||
fetchGoogleMeetAttendance({
|
||||
accessToken: "token",
|
||||
conferenceRecord: "rec-1",
|
||||
pageSize: 3,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
input: "rec-1",
|
||||
conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
|
||||
attendance: [
|
||||
{
|
||||
conferenceRecord: "conferenceRecords/rec-1",
|
||||
participant: "conferenceRecords/rec-1/participants/p1",
|
||||
displayName: "Alice",
|
||||
user: "users/alice",
|
||||
sessions: [
|
||||
{
|
||||
name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"https://meet.googleapis.com/v2/conferenceRecords/rec-1",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: "Bearer token" }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces Developer Preview acknowledgment blockers in preflight reports", () => {
|
||||
expect(
|
||||
buildGoogleMeetPreflightReport({
|
||||
@@ -454,6 +640,27 @@ describe("google-meet plugin", () => {
|
||||
expect(result.details.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("reports attendance through the tool", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const { tools } = setup();
|
||||
const tool = tools[0] as {
|
||||
execute: (
|
||||
id: string,
|
||||
params: unknown,
|
||||
) => Promise<{ details: { attendance?: Array<{ displayName?: string }> } }>;
|
||||
};
|
||||
|
||||
const result = await tool.execute("id", {
|
||||
action: "attendance",
|
||||
accessToken: "token",
|
||||
expiresAt: Date.now() + 120_000,
|
||||
conferenceRecord: "rec-1",
|
||||
pageSize: 3,
|
||||
});
|
||||
|
||||
expect(result.details.attendance).toEqual([expect.objectContaining({ displayName: "Alice" })]);
|
||||
});
|
||||
|
||||
it("fails setup status when the configured Chrome node is not connected", async () => {
|
||||
const { tools } = setup(
|
||||
{
|
||||
@@ -630,6 +837,81 @@ describe("google-meet plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI artifacts prints JSON output", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"artifacts",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
"--json",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(JSON.parse(stdout.output())).toMatchObject({
|
||||
conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
|
||||
artifacts: [
|
||||
{
|
||||
recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
|
||||
transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
|
||||
smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
|
||||
},
|
||||
],
|
||||
tokenSource: "cached-access-token",
|
||||
});
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI attendance prints participant sessions by default", async () => {
|
||||
stubMeetArtifactsApi();
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
registerGoogleMeetCli({
|
||||
program,
|
||||
config: resolveGoogleMeetConfig({}),
|
||||
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
|
||||
});
|
||||
|
||||
try {
|
||||
await program.parseAsync(
|
||||
[
|
||||
"googlemeet",
|
||||
"attendance",
|
||||
"--access-token",
|
||||
"token",
|
||||
"--expires-at",
|
||||
String(Date.now() + 120_000),
|
||||
"--conference-record",
|
||||
"rec-1",
|
||||
],
|
||||
{ from: "user" },
|
||||
);
|
||||
expect(stdout.output()).toContain("attendance rows: 1");
|
||||
expect(stdout.output()).toContain("participant: Alice");
|
||||
expect(stdout.output()).toContain(
|
||||
"conferenceRecords/rec-1/participants/p1/participantSessions/s1",
|
||||
);
|
||||
} finally {
|
||||
stdout.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("CLI doctor prints human-readable session health", async () => {
|
||||
const program = new Command();
|
||||
const stdout = captureStdout();
|
||||
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
createMeetFromParams,
|
||||
shouldJoinCreatedMeet,
|
||||
} from "./src/create.js";
|
||||
import { buildGoogleMeetPreflightReport, fetchGoogleMeetSpace } from "./src/meet.js";
|
||||
import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchGoogleMeetSpace,
|
||||
} from "./src/meet.js";
|
||||
import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
|
||||
import { resolveGoogleMeetAccessToken } from "./src/oauth.js";
|
||||
import { GoogleMeetRuntime } from "./src/runtime.js";
|
||||
@@ -145,6 +150,8 @@ const GoogleMeetToolSchema = Type.Object({
|
||||
"setup_status",
|
||||
"resolve_space",
|
||||
"preflight",
|
||||
"artifacts",
|
||||
"attendance",
|
||||
"recover_current_tab",
|
||||
"leave",
|
||||
"speak",
|
||||
@@ -175,6 +182,10 @@ const GoogleMeetToolSchema = Type.Object({
|
||||
sessionId: Type.Optional(Type.String({ description: "Meet session ID" })),
|
||||
message: Type.Optional(Type.String({ description: "Realtime instructions to speak now" })),
|
||||
meeting: Type.Optional(Type.String({ description: "Meet URL, meeting code, or spaces/{id}" })),
|
||||
conferenceRecord: Type.Optional(
|
||||
Type.String({ description: "Meet conferenceRecords/{id} resource name or id" }),
|
||||
),
|
||||
pageSize: Type.Optional(Type.Number({ description: "Meet API page size for list actions" })),
|
||||
accessToken: Type.Optional(Type.String({ description: "Access token override" })),
|
||||
refreshToken: Type.Optional(Type.String({ description: "Refresh token override" })),
|
||||
clientId: Type.Optional(Type.String({ description: "OAuth client id override" })),
|
||||
@@ -211,15 +222,33 @@ function resolveMeetingInput(config: GoogleMeetConfig, value: unknown): string {
|
||||
return meeting;
|
||||
}
|
||||
|
||||
async function resolveSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
|
||||
const meeting = resolveMeetingInput(config, raw.meeting);
|
||||
const token = await resolveGoogleMeetAccessToken({
|
||||
function resolveOptionalPositiveInteger(value: unknown): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = typeof value === "number" ? value : Number(normalizeOptionalString(value));
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) {
|
||||
throw new Error("Expected pageSize to be a positive integer");
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function resolveGoogleMeetTokenFromParams(
|
||||
config: GoogleMeetConfig,
|
||||
raw: Record<string, unknown>,
|
||||
) {
|
||||
return resolveGoogleMeetAccessToken({
|
||||
clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
|
||||
clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
|
||||
refreshToken: normalizeOptionalString(raw.refreshToken) ?? config.oauth.refreshToken,
|
||||
accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
|
||||
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSpaceFromParams(config: GoogleMeetConfig, raw: Record<string, unknown>) {
|
||||
const meeting = resolveMeetingInput(config, raw.meeting);
|
||||
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
||||
const space = await fetchGoogleMeetSpace({
|
||||
accessToken: token.accessToken,
|
||||
meeting,
|
||||
@@ -227,6 +256,24 @@ async function resolveSpaceFromParams(config: GoogleMeetConfig, raw: Record<stri
|
||||
return { meeting, token, space };
|
||||
}
|
||||
|
||||
async function resolveArtifactQueryFromParams(
|
||||
config: GoogleMeetConfig,
|
||||
raw: Record<string, unknown>,
|
||||
) {
|
||||
const meeting = normalizeOptionalString(raw.meeting) ?? config.defaults.meeting;
|
||||
const conferenceRecord = normalizeOptionalString(raw.conferenceRecord);
|
||||
if (!meeting && !conferenceRecord) {
|
||||
throw new Error("Meeting input or conferenceRecord required");
|
||||
}
|
||||
const token = await resolveGoogleMeetTokenFromParams(config, raw);
|
||||
return {
|
||||
token,
|
||||
meeting,
|
||||
conferenceRecord,
|
||||
pageSize: resolveOptionalPositiveInteger(raw.pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "google-meet",
|
||||
name: "Google Meet",
|
||||
@@ -337,6 +384,48 @@ export default definePluginEntry({
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"googlemeet.artifacts",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asParamRecord(params);
|
||||
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
||||
respond(
|
||||
true,
|
||||
await fetchGoogleMeetArtifacts({
|
||||
accessToken: resolved.token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
sendError(respond, err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"googlemeet.attendance",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
try {
|
||||
const raw = asParamRecord(params);
|
||||
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
||||
respond(
|
||||
true,
|
||||
await fetchGoogleMeetAttendance({
|
||||
accessToken: resolved.token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
sendError(respond, err);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"googlemeet.leave",
|
||||
async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
||||
@@ -469,6 +558,28 @@ export default definePluginEntry({
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "artifacts": {
|
||||
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
||||
return json(
|
||||
await fetchGoogleMeetArtifacts({
|
||||
accessToken: resolved.token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "attendance": {
|
||||
const resolved = await resolveArtifactQueryFromParams(config, raw);
|
||||
return json(
|
||||
await fetchGoogleMeetAttendance({
|
||||
accessToken: resolved.token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "leave": {
|
||||
const rt = await ensureRuntime();
|
||||
const sessionId = normalizeOptionalString(raw.sessionId);
|
||||
|
||||
@@ -5,7 +5,11 @@ import type { GoogleMeetConfig, GoogleMeetMode, GoogleMeetTransport } from "./co
|
||||
import {
|
||||
buildGoogleMeetPreflightReport,
|
||||
createGoogleMeetSpace,
|
||||
fetchGoogleMeetArtifacts,
|
||||
fetchGoogleMeetAttendance,
|
||||
fetchGoogleMeetSpace,
|
||||
type GoogleMeetArtifactsResult,
|
||||
type GoogleMeetAttendanceResult,
|
||||
} from "./meet.js";
|
||||
import {
|
||||
buildGoogleMeetAuthUrl,
|
||||
@@ -44,6 +48,11 @@ type ResolveSpaceOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
|
||||
type MeetArtifactOptions = ResolveSpaceOptions & {
|
||||
conferenceRecord?: string;
|
||||
pageSize?: string;
|
||||
};
|
||||
|
||||
type SetupOptions = {
|
||||
json?: boolean;
|
||||
};
|
||||
@@ -251,6 +260,38 @@ function resolveCreateTokenOptions(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveArtifactTokenOptions(
|
||||
config: GoogleMeetConfig,
|
||||
options: MeetArtifactOptions,
|
||||
): {
|
||||
meeting?: string;
|
||||
conferenceRecord?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
refreshToken?: string;
|
||||
accessToken?: string;
|
||||
expiresAt?: number;
|
||||
pageSize?: number;
|
||||
} {
|
||||
const meeting = options.meeting?.trim() || config.defaults.meeting;
|
||||
const conferenceRecord = options.conferenceRecord?.trim();
|
||||
if (!meeting && !conferenceRecord) {
|
||||
throw new Error(
|
||||
"Meeting input or conference record is required. Pass --meeting, --conference-record, or configure defaults.meeting.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
meeting,
|
||||
conferenceRecord,
|
||||
clientId: options.clientId?.trim() || config.oauth.clientId,
|
||||
clientSecret: options.clientSecret?.trim() || config.oauth.clientSecret,
|
||||
refreshToken: options.refreshToken?.trim() || config.oauth.refreshToken,
|
||||
accessToken: options.accessToken?.trim() || config.oauth.accessToken,
|
||||
expiresAt: parseOptionalNumber(options.expiresAt) ?? config.oauth.expiresAt,
|
||||
pageSize: parseOptionalNumber(options.pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
function hasCreateOAuth(config: GoogleMeetConfig, options: CreateOptions): boolean {
|
||||
return Boolean(
|
||||
options.accessToken?.trim() ||
|
||||
@@ -260,6 +301,67 @@ function hasCreateOAuth(config: GoogleMeetConfig, options: CreateOptions): boole
|
||||
);
|
||||
}
|
||||
|
||||
function writeArtifactsSummary(result: GoogleMeetArtifactsResult): void {
|
||||
if (result.input) {
|
||||
writeStdoutLine("input: %s", result.input);
|
||||
}
|
||||
if (result.space) {
|
||||
writeStdoutLine("space: %s", result.space.name);
|
||||
}
|
||||
writeStdoutLine("conference records: %d", result.conferenceRecords.length);
|
||||
for (const entry of result.artifacts) {
|
||||
writeStdoutLine("");
|
||||
writeStdoutLine("record: %s", entry.conferenceRecord.name);
|
||||
writeStdoutLine("started: %s", formatOptional(entry.conferenceRecord.startTime));
|
||||
writeStdoutLine("ended: %s", formatOptional(entry.conferenceRecord.endTime));
|
||||
writeStdoutLine("participants: %d", entry.participants.length);
|
||||
writeStdoutLine("recordings: %d", entry.recordings.length);
|
||||
writeStdoutLine("transcripts: %d", entry.transcripts.length);
|
||||
writeStdoutLine("smart notes: %d", entry.smartNotes.length);
|
||||
if (entry.smartNotesError) {
|
||||
writeStdoutLine("smart notes warning: %s", entry.smartNotesError);
|
||||
}
|
||||
for (const recording of entry.recordings) {
|
||||
writeStdoutLine("- recording: %s", recording.name);
|
||||
}
|
||||
for (const transcript of entry.transcripts) {
|
||||
writeStdoutLine("- transcript: %s", transcript.name);
|
||||
}
|
||||
for (const smartNote of entry.smartNotes) {
|
||||
writeStdoutLine("- smart note: %s", smartNote.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeAttendanceSummary(result: GoogleMeetAttendanceResult): void {
|
||||
if (result.input) {
|
||||
writeStdoutLine("input: %s", result.input);
|
||||
}
|
||||
if (result.space) {
|
||||
writeStdoutLine("space: %s", result.space.name);
|
||||
}
|
||||
writeStdoutLine("conference records: %d", result.conferenceRecords.length);
|
||||
writeStdoutLine("attendance rows: %d", result.attendance.length);
|
||||
for (const row of result.attendance) {
|
||||
const identity = row.displayName || row.user || row.participant;
|
||||
writeStdoutLine("");
|
||||
writeStdoutLine("participant: %s", identity);
|
||||
writeStdoutLine("record: %s", row.conferenceRecord);
|
||||
writeStdoutLine("resource: %s", row.participant);
|
||||
writeStdoutLine("first joined: %s", formatOptional(row.earliestStartTime));
|
||||
writeStdoutLine("last left: %s", formatOptional(row.latestEndTime));
|
||||
writeStdoutLine("sessions: %d", row.sessions.length);
|
||||
for (const session of row.sessions) {
|
||||
writeStdoutLine(
|
||||
"- %s: %s -> %s",
|
||||
session.name,
|
||||
formatOptional(session.startTime),
|
||||
formatOptional(session.endTime),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerGoogleMeetCli(params: {
|
||||
program: Command;
|
||||
config: GoogleMeetConfig;
|
||||
@@ -570,6 +672,76 @@ export function registerGoogleMeetCli(params: {
|
||||
}
|
||||
});
|
||||
|
||||
root
|
||||
.command("artifacts")
|
||||
.description("List Meet conference records and available participant/artifact metadata")
|
||||
.option("--meeting <value>", "Meet URL, meeting code, or spaces/{id}")
|
||||
.option("--conference-record <name>", "Conference record name or id")
|
||||
.option("--access-token <token>", "Access token override")
|
||||
.option("--refresh-token <token>", "Refresh token override")
|
||||
.option("--client-id <id>", "OAuth client id override")
|
||||
.option("--client-secret <secret>", "OAuth client secret override")
|
||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||
.option("--page-size <n>", "Max resources per Meet API page")
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: MeetArtifactOptions) => {
|
||||
const resolved = resolveArtifactTokenOptions(params.config, options);
|
||||
const token = await resolveGoogleMeetAccessToken(resolved);
|
||||
const result = await fetchGoogleMeetArtifacts({
|
||||
accessToken: token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
});
|
||||
if (options.json) {
|
||||
writeStdoutJson({
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
return;
|
||||
}
|
||||
writeArtifactsSummary(result);
|
||||
writeStdoutLine(
|
||||
"token source: %s",
|
||||
token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
);
|
||||
});
|
||||
|
||||
root
|
||||
.command("attendance")
|
||||
.description("List Meet participants and participant sessions")
|
||||
.option("--meeting <value>", "Meet URL, meeting code, or spaces/{id}")
|
||||
.option("--conference-record <name>", "Conference record name or id")
|
||||
.option("--access-token <token>", "Access token override")
|
||||
.option("--refresh-token <token>", "Refresh token override")
|
||||
.option("--client-id <id>", "OAuth client id override")
|
||||
.option("--client-secret <secret>", "OAuth client secret override")
|
||||
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
|
||||
.option("--page-size <n>", "Max resources per Meet API page")
|
||||
.option("--json", "Print JSON output", false)
|
||||
.action(async (options: MeetArtifactOptions) => {
|
||||
const resolved = resolveArtifactTokenOptions(params.config, options);
|
||||
const token = await resolveGoogleMeetAccessToken(resolved);
|
||||
const result = await fetchGoogleMeetAttendance({
|
||||
accessToken: token.accessToken,
|
||||
meeting: resolved.meeting,
|
||||
conferenceRecord: resolved.conferenceRecord,
|
||||
pageSize: resolved.pageSize,
|
||||
});
|
||||
if (options.json) {
|
||||
writeStdoutJson({
|
||||
...result,
|
||||
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
});
|
||||
return;
|
||||
}
|
||||
writeAttendanceSummary(result);
|
||||
writeStdoutLine(
|
||||
"token source: %s",
|
||||
token.refreshed ? "refresh-token" : "cached-access-token",
|
||||
);
|
||||
});
|
||||
|
||||
root
|
||||
.command("status")
|
||||
.argument("[session-id]", "Meet session ID")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
const GOOGLE_MEET_API_BASE_URL = "https://meet.googleapis.com/v2";
|
||||
const GOOGLE_MEET_API_ORIGIN = "https://meet.googleapis.com";
|
||||
const GOOGLE_MEET_API_BASE_URL = `${GOOGLE_MEET_API_ORIGIN}/v2`;
|
||||
const GOOGLE_MEET_URL_HOST = "meet.google.com";
|
||||
const GOOGLE_MEET_API_HOST = "meet.googleapis.com";
|
||||
|
||||
@@ -28,6 +29,95 @@ export type GoogleMeetCreateSpaceResult = {
|
||||
meetingUri: string;
|
||||
};
|
||||
|
||||
export type GoogleMeetConferenceRecord = {
|
||||
name: string;
|
||||
space?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
expireTime?: string;
|
||||
};
|
||||
|
||||
export type GoogleMeetParticipant = {
|
||||
name: string;
|
||||
earliestStartTime?: string;
|
||||
latestEndTime?: string;
|
||||
signedinUser?: {
|
||||
user?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
anonymousUser?: {
|
||||
displayName?: string;
|
||||
};
|
||||
phoneUser?: {
|
||||
displayName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type GoogleMeetParticipantSession = {
|
||||
name: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
};
|
||||
|
||||
export type GoogleMeetRecording = {
|
||||
name: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
driveDestination?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type GoogleMeetTranscript = {
|
||||
name: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
docsDestination?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type GoogleMeetSmartNote = {
|
||||
name: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
docsDestination?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type GoogleMeetArtifactsEntry = {
|
||||
conferenceRecord: GoogleMeetConferenceRecord;
|
||||
participants: GoogleMeetParticipant[];
|
||||
recordings: GoogleMeetRecording[];
|
||||
transcripts: GoogleMeetTranscript[];
|
||||
smartNotes: GoogleMeetSmartNote[];
|
||||
smartNotesError?: string;
|
||||
};
|
||||
|
||||
export type GoogleMeetArtifactsResult = {
|
||||
input?: string;
|
||||
space?: GoogleMeetSpace;
|
||||
conferenceRecords: GoogleMeetConferenceRecord[];
|
||||
artifacts: GoogleMeetArtifactsEntry[];
|
||||
};
|
||||
|
||||
export type GoogleMeetAttendanceRow = {
|
||||
conferenceRecord: string;
|
||||
participant: string;
|
||||
displayName?: string;
|
||||
user?: string;
|
||||
earliestStartTime?: string;
|
||||
latestEndTime?: string;
|
||||
sessions: GoogleMeetParticipantSession[];
|
||||
};
|
||||
|
||||
export type GoogleMeetAttendanceResult = {
|
||||
input?: string;
|
||||
space?: GoogleMeetSpace;
|
||||
conferenceRecords: GoogleMeetConferenceRecord[];
|
||||
attendance: GoogleMeetAttendanceRow[];
|
||||
};
|
||||
|
||||
type GoogleMeetSmartNotesListResult = {
|
||||
smartNotes: GoogleMeetSmartNote[];
|
||||
smartNotesError?: string;
|
||||
};
|
||||
|
||||
export function normalizeGoogleMeetSpaceName(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
@@ -61,6 +151,121 @@ function encodeSpaceNameForPath(name: string): string {
|
||||
return name.split("/").map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
||||
function encodeResourceNameForPath(name: string): string {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Google Meet resource name is required");
|
||||
}
|
||||
return trimmed.split("/").map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
||||
function normalizeConferenceRecordName(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("Conference record is required");
|
||||
}
|
||||
return trimmed.startsWith("conferenceRecords/") ? trimmed : `conferenceRecords/${trimmed}`;
|
||||
}
|
||||
|
||||
function appendQuery(
|
||||
url: string,
|
||||
query?: Record<string, string | number | boolean | undefined>,
|
||||
): string {
|
||||
if (!query) {
|
||||
return url;
|
||||
}
|
||||
const parsed = new URL(url);
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== undefined) {
|
||||
parsed.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
function assertResourceArray<T extends { name?: string }>(
|
||||
value: unknown,
|
||||
key: string,
|
||||
context: string,
|
||||
): T[] {
|
||||
if (value === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Google Meet ${context} response had non-array ${key}`);
|
||||
}
|
||||
const resources = value as T[];
|
||||
for (const resource of resources) {
|
||||
if (!resource.name?.trim()) {
|
||||
throw new Error(`Google Meet ${context} response included a resource without name`);
|
||||
}
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
async function fetchGoogleMeetJson<T>(params: {
|
||||
accessToken: string;
|
||||
path: string;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
auditContext: string;
|
||||
errorPrefix: string;
|
||||
}): Promise<T> {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: appendQuery(`${GOOGLE_MEET_API_BASE_URL}/${params.path}`, params.query),
|
||||
init: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
policy: { allowedHostnames: [GOOGLE_MEET_API_HOST] },
|
||||
auditContext: params.auditContext,
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(`${params.errorPrefix} failed (${response.status}): ${detail}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function listGoogleMeetCollection<T extends { name?: string }>(params: {
|
||||
accessToken: string;
|
||||
path: string;
|
||||
collectionKey: string;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
auditContext: string;
|
||||
errorPrefix: string;
|
||||
}): Promise<T[]> {
|
||||
const items: T[] = [];
|
||||
let pageToken: string | undefined;
|
||||
do {
|
||||
const payload = await fetchGoogleMeetJson<Record<string, unknown>>({
|
||||
accessToken: params.accessToken,
|
||||
path: params.path,
|
||||
query: { ...params.query, pageToken },
|
||||
auditContext: params.auditContext,
|
||||
errorPrefix: params.errorPrefix,
|
||||
});
|
||||
items.push(
|
||||
...assertResourceArray<T>(
|
||||
payload[params.collectionKey],
|
||||
params.collectionKey,
|
||||
params.errorPrefix,
|
||||
),
|
||||
);
|
||||
pageToken = typeof payload.nextPageToken === "string" ? payload.nextPageToken : undefined;
|
||||
} while (pageToken);
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function fetchGoogleMeetSpace(params: {
|
||||
accessToken: string;
|
||||
meeting: string;
|
||||
@@ -128,6 +333,269 @@ export async function createGoogleMeetSpace(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchGoogleMeetConferenceRecord(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
}): Promise<GoogleMeetConferenceRecord> {
|
||||
const name = normalizeConferenceRecordName(params.conferenceRecord);
|
||||
const payload = await fetchGoogleMeetJson<GoogleMeetConferenceRecord>({
|
||||
accessToken: params.accessToken,
|
||||
path: encodeResourceNameForPath(name),
|
||||
auditContext: "google-meet.conferenceRecords.get",
|
||||
errorPrefix: "Google Meet conferenceRecords.get",
|
||||
});
|
||||
if (!payload.name?.trim()) {
|
||||
throw new Error("Google Meet conferenceRecords.get response was missing name");
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function listGoogleMeetConferenceRecords(params: {
|
||||
accessToken: string;
|
||||
meeting?: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetConferenceRecord[]> {
|
||||
const filter = params.meeting
|
||||
? `space.name = "${normalizeGoogleMeetSpaceName(params.meeting)}"`
|
||||
: undefined;
|
||||
return listGoogleMeetCollection<GoogleMeetConferenceRecord>({
|
||||
accessToken: params.accessToken,
|
||||
path: "conferenceRecords",
|
||||
collectionKey: "conferenceRecords",
|
||||
query: {
|
||||
pageSize: params.pageSize,
|
||||
filter,
|
||||
},
|
||||
auditContext: "google-meet.conferenceRecords.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleMeetParticipants(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetParticipant[]> {
|
||||
const parent = normalizeConferenceRecordName(params.conferenceRecord);
|
||||
return listGoogleMeetCollection<GoogleMeetParticipant>({
|
||||
accessToken: params.accessToken,
|
||||
path: `${encodeResourceNameForPath(parent)}/participants`,
|
||||
collectionKey: "participants",
|
||||
query: { pageSize: params.pageSize },
|
||||
auditContext: "google-meet.conferenceRecords.participants.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.participants.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleMeetParticipantSessions(params: {
|
||||
accessToken: string;
|
||||
participant: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetParticipantSession[]> {
|
||||
return listGoogleMeetCollection<GoogleMeetParticipantSession>({
|
||||
accessToken: params.accessToken,
|
||||
path: `${encodeResourceNameForPath(params.participant)}/participantSessions`,
|
||||
collectionKey: "participantSessions",
|
||||
query: { pageSize: params.pageSize },
|
||||
auditContext: "google-meet.conferenceRecords.participants.participantSessions.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.participants.participantSessions.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleMeetRecordings(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetRecording[]> {
|
||||
const parent = normalizeConferenceRecordName(params.conferenceRecord);
|
||||
return listGoogleMeetCollection<GoogleMeetRecording>({
|
||||
accessToken: params.accessToken,
|
||||
path: `${encodeResourceNameForPath(parent)}/recordings`,
|
||||
collectionKey: "recordings",
|
||||
query: { pageSize: params.pageSize },
|
||||
auditContext: "google-meet.conferenceRecords.recordings.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.recordings.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleMeetTranscripts(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetTranscript[]> {
|
||||
const parent = normalizeConferenceRecordName(params.conferenceRecord);
|
||||
return listGoogleMeetCollection<GoogleMeetTranscript>({
|
||||
accessToken: params.accessToken,
|
||||
path: `${encodeResourceNameForPath(parent)}/transcripts`,
|
||||
collectionKey: "transcripts",
|
||||
query: { pageSize: params.pageSize },
|
||||
auditContext: "google-meet.conferenceRecords.transcripts.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.transcripts.list",
|
||||
});
|
||||
}
|
||||
|
||||
export async function listGoogleMeetSmartNotes(params: {
|
||||
accessToken: string;
|
||||
conferenceRecord: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetSmartNote[]> {
|
||||
const parent = normalizeConferenceRecordName(params.conferenceRecord);
|
||||
return listGoogleMeetCollection<GoogleMeetSmartNote>({
|
||||
accessToken: params.accessToken,
|
||||
path: `${encodeResourceNameForPath(parent)}/smartNotes`,
|
||||
collectionKey: "smartNotes",
|
||||
query: { pageSize: params.pageSize },
|
||||
auditContext: "google-meet.conferenceRecords.smartNotes.list",
|
||||
errorPrefix: "Google Meet conferenceRecords.smartNotes.list",
|
||||
});
|
||||
}
|
||||
|
||||
function getParticipantDisplayName(participant: GoogleMeetParticipant): string | undefined {
|
||||
return (
|
||||
participant.signedinUser?.displayName ??
|
||||
participant.anonymousUser?.displayName ??
|
||||
participant.phoneUser?.displayName
|
||||
);
|
||||
}
|
||||
|
||||
function getParticipantUser(participant: GoogleMeetParticipant): string | undefined {
|
||||
return participant.signedinUser?.user;
|
||||
}
|
||||
|
||||
async function resolveConferenceRecordQuery(params: {
|
||||
accessToken: string;
|
||||
meeting?: string;
|
||||
conferenceRecord?: string;
|
||||
pageSize?: number;
|
||||
}): Promise<{
|
||||
input?: string;
|
||||
space?: GoogleMeetSpace;
|
||||
conferenceRecords: GoogleMeetConferenceRecord[];
|
||||
}> {
|
||||
if (params.conferenceRecord?.trim()) {
|
||||
const conferenceRecord = await fetchGoogleMeetConferenceRecord({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: params.conferenceRecord,
|
||||
});
|
||||
return {
|
||||
input: params.conferenceRecord.trim(),
|
||||
conferenceRecords: [conferenceRecord],
|
||||
};
|
||||
}
|
||||
if (!params.meeting?.trim()) {
|
||||
throw new Error("Meeting input or conference record is required");
|
||||
}
|
||||
const space = await fetchGoogleMeetSpace({
|
||||
accessToken: params.accessToken,
|
||||
meeting: params.meeting,
|
||||
});
|
||||
const conferenceRecords = await listGoogleMeetConferenceRecords({
|
||||
accessToken: params.accessToken,
|
||||
meeting: space.name,
|
||||
pageSize: params.pageSize,
|
||||
});
|
||||
return {
|
||||
input: params.meeting,
|
||||
space,
|
||||
conferenceRecords,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGoogleMeetArtifacts(params: {
|
||||
accessToken: string;
|
||||
meeting?: string;
|
||||
conferenceRecord?: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetArtifactsResult> {
|
||||
const resolved = await resolveConferenceRecordQuery(params);
|
||||
const artifacts = await Promise.all(
|
||||
resolved.conferenceRecords.map(async (conferenceRecord) => {
|
||||
const [participants, recordings, transcripts, smartNotesResult] = await Promise.all([
|
||||
listGoogleMeetParticipants({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
listGoogleMeetRecordings({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
listGoogleMeetTranscripts({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
listGoogleMeetSmartNotes({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
pageSize: params.pageSize,
|
||||
})
|
||||
.then<GoogleMeetSmartNotesListResult>((smartNotes) => ({ smartNotes }))
|
||||
.catch((error: unknown) => ({
|
||||
smartNotes: [],
|
||||
smartNotesError: getErrorMessage(error),
|
||||
})),
|
||||
]);
|
||||
return {
|
||||
conferenceRecord,
|
||||
participants,
|
||||
recordings,
|
||||
transcripts,
|
||||
smartNotes: smartNotesResult.smartNotes,
|
||||
...(smartNotesResult.smartNotesError
|
||||
? { smartNotesError: smartNotesResult.smartNotesError }
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
return {
|
||||
input: resolved.input,
|
||||
space: resolved.space,
|
||||
conferenceRecords: resolved.conferenceRecords,
|
||||
artifacts,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGoogleMeetAttendance(params: {
|
||||
accessToken: string;
|
||||
meeting?: string;
|
||||
conferenceRecord?: string;
|
||||
pageSize?: number;
|
||||
}): Promise<GoogleMeetAttendanceResult> {
|
||||
const resolved = await resolveConferenceRecordQuery(params);
|
||||
const nestedRows = await Promise.all(
|
||||
resolved.conferenceRecords.map(async (conferenceRecord) => {
|
||||
const participants = await listGoogleMeetParticipants({
|
||||
accessToken: params.accessToken,
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
pageSize: params.pageSize,
|
||||
});
|
||||
return Promise.all(
|
||||
participants.map(async (participant) => ({
|
||||
conferenceRecord: conferenceRecord.name,
|
||||
participant: participant.name,
|
||||
displayName: getParticipantDisplayName(participant),
|
||||
user: getParticipantUser(participant),
|
||||
earliestStartTime: participant.earliestStartTime,
|
||||
latestEndTime: participant.latestEndTime,
|
||||
sessions: await listGoogleMeetParticipantSessions({
|
||||
accessToken: params.accessToken,
|
||||
participant: participant.name,
|
||||
pageSize: params.pageSize,
|
||||
}),
|
||||
})),
|
||||
);
|
||||
}),
|
||||
);
|
||||
return {
|
||||
input: resolved.input,
|
||||
space: resolved.space,
|
||||
conferenceRecords: resolved.conferenceRecords,
|
||||
attendance: nestedRows.flat(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildGoogleMeetPreflightReport(params: {
|
||||
input: string;
|
||||
space: GoogleMeetSpace;
|
||||
|
||||
Reference in New Issue
Block a user