@gakr-gakr/google-meet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autobot.plugin.json +532 -0
- package/doctor-contract-api.ts +1 -0
- package/index.ts +1224 -0
- package/package.json +47 -0
- package/src/agent-consult.ts +158 -0
- package/src/calendar.ts +252 -0
- package/src/cli.ts +2350 -0
- package/src/config-compat.ts +84 -0
- package/src/config.ts +589 -0
- package/src/create.ts +157 -0
- package/src/drive.ts +72 -0
- package/src/google-api-errors.ts +20 -0
- package/src/meet.ts +1024 -0
- package/src/node-host.ts +520 -0
- package/src/oauth.ts +229 -0
- package/src/realtime-node.ts +780 -0
- package/src/realtime.ts +1334 -0
- package/src/runtime.ts +1008 -0
- package/src/setup.ts +285 -0
- package/src/transports/chrome-browser-proxy.ts +204 -0
- package/src/transports/chrome-create.ts +364 -0
- package/src/transports/chrome.ts +1065 -0
- package/src/transports/twilio.ts +57 -0
- package/src/transports/types.ts +147 -0
- package/src/voice-call-gateway.ts +241 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import type { PluginRuntime } from "autobot/plugin-sdk/plugin-runtime";
|
|
2
|
+
import { sleep } from "autobot/plugin-sdk/runtime-env";
|
|
3
|
+
import type { GoogleMeetConfig } from "../config.js";
|
|
4
|
+
import {
|
|
5
|
+
asBrowserTabs,
|
|
6
|
+
callBrowserProxyOnNode,
|
|
7
|
+
readBrowserTab,
|
|
8
|
+
resolveChromeNode,
|
|
9
|
+
type BrowserTab,
|
|
10
|
+
} from "./chrome-browser-proxy.js";
|
|
11
|
+
import type { GoogleMeetChromeHealth } from "./types.js";
|
|
12
|
+
|
|
13
|
+
const GOOGLE_MEET_NEW_URL = "https://meet.google.com/new";
|
|
14
|
+
const GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS = 60_000;
|
|
15
|
+
const GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS = 10_000;
|
|
16
|
+
const GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS = 1_000;
|
|
17
|
+
const GOOGLE_MEET_BROWSER_POLL_MS = 500;
|
|
18
|
+
|
|
19
|
+
type BrowserCreateStepResult = {
|
|
20
|
+
meetingUri?: string;
|
|
21
|
+
browserUrl?: string;
|
|
22
|
+
browserTitle?: string;
|
|
23
|
+
manualAction?: string;
|
|
24
|
+
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
|
|
25
|
+
notes?: string[];
|
|
26
|
+
retryAfterMs?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type GoogleMeetBrowserCreateResult = {
|
|
30
|
+
meetingUri: string;
|
|
31
|
+
nodeId: string;
|
|
32
|
+
targetId?: string;
|
|
33
|
+
browserUrl?: string;
|
|
34
|
+
browserTitle?: string;
|
|
35
|
+
notes?: string[];
|
|
36
|
+
source: "browser";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type GoogleMeetBrowserManualAction = {
|
|
40
|
+
source: "browser";
|
|
41
|
+
error: string;
|
|
42
|
+
manualActionRequired: true;
|
|
43
|
+
manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
|
|
44
|
+
manualActionMessage: string;
|
|
45
|
+
browser: {
|
|
46
|
+
nodeId: string;
|
|
47
|
+
targetId?: string;
|
|
48
|
+
browserUrl?: string;
|
|
49
|
+
browserTitle?: string;
|
|
50
|
+
notes?: string[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
class GoogleMeetBrowserManualActionError extends Error {
|
|
55
|
+
readonly payload: GoogleMeetBrowserManualAction;
|
|
56
|
+
|
|
57
|
+
constructor(payload: Omit<GoogleMeetBrowserManualAction, "source" | "error">) {
|
|
58
|
+
const prefix = payload.manualActionReason ? `${payload.manualActionReason}: ` : "";
|
|
59
|
+
super(`${prefix}${payload.manualActionMessage}`);
|
|
60
|
+
this.name = "GoogleMeetBrowserManualActionError";
|
|
61
|
+
this.payload = {
|
|
62
|
+
source: "browser",
|
|
63
|
+
error: this.message,
|
|
64
|
+
...payload,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function isGoogleMeetBrowserManualActionError(
|
|
70
|
+
error: unknown,
|
|
71
|
+
): error is GoogleMeetBrowserManualActionError {
|
|
72
|
+
return error instanceof GoogleMeetBrowserManualActionError;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatBrowserAutomationError(error: unknown): string {
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
return error.message;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return JSON.stringify(error);
|
|
81
|
+
} catch {
|
|
82
|
+
return "unknown error";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isBrowserNavigationInterruption(error: unknown): boolean {
|
|
87
|
+
return /execution context was destroyed|navigation|target closed/i.test(
|
|
88
|
+
formatBrowserAutomationError(error),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isGoogleMeetCreateTab(tab: BrowserTab): boolean {
|
|
93
|
+
const url = tab.url ?? "";
|
|
94
|
+
if (/^https:\/\/meet\.google\.com\/(?:new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.test(url)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
url.startsWith("https://accounts.google.com/") &&
|
|
99
|
+
/sign in|google accounts|meet/i.test(tab.title ?? "")
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function findGoogleMeetCreateTab(params: {
|
|
104
|
+
runtime: PluginRuntime;
|
|
105
|
+
nodeId: string;
|
|
106
|
+
timeoutMs: number;
|
|
107
|
+
}): Promise<BrowserTab | undefined> {
|
|
108
|
+
const tabs = asBrowserTabs(
|
|
109
|
+
await callBrowserProxyOnNode({
|
|
110
|
+
runtime: params.runtime,
|
|
111
|
+
nodeId: params.nodeId,
|
|
112
|
+
method: "GET",
|
|
113
|
+
path: "/tabs",
|
|
114
|
+
timeoutMs: params.timeoutMs,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
return tabs.find(isGoogleMeetCreateTab);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function focusBrowserTab(params: {
|
|
121
|
+
runtime: PluginRuntime;
|
|
122
|
+
nodeId: string;
|
|
123
|
+
targetId: string;
|
|
124
|
+
timeoutMs: number;
|
|
125
|
+
}): Promise<void> {
|
|
126
|
+
await callBrowserProxyOnNode({
|
|
127
|
+
runtime: params.runtime,
|
|
128
|
+
nodeId: params.nodeId,
|
|
129
|
+
method: "POST",
|
|
130
|
+
path: "/tabs/focus",
|
|
131
|
+
body: { targetId: params.targetId },
|
|
132
|
+
timeoutMs: params.timeoutMs,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readStringArray(value: unknown): string[] | undefined {
|
|
137
|
+
return Array.isArray(value)
|
|
138
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
139
|
+
: undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readBrowserCreateResult(result: unknown): BrowserCreateStepResult {
|
|
143
|
+
const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
|
|
144
|
+
const nested =
|
|
145
|
+
record.result && typeof record.result === "object"
|
|
146
|
+
? (record.result as Record<string, unknown>)
|
|
147
|
+
: record;
|
|
148
|
+
return {
|
|
149
|
+
meetingUri: typeof nested.meetingUri === "string" ? nested.meetingUri : undefined,
|
|
150
|
+
browserUrl: typeof nested.browserUrl === "string" ? nested.browserUrl : undefined,
|
|
151
|
+
browserTitle: typeof nested.browserTitle === "string" ? nested.browserTitle : undefined,
|
|
152
|
+
manualAction: typeof nested.manualAction === "string" ? nested.manualAction : undefined,
|
|
153
|
+
manualActionReason:
|
|
154
|
+
typeof nested.manualActionReason === "string"
|
|
155
|
+
? (nested.manualActionReason as GoogleMeetChromeHealth["manualActionReason"])
|
|
156
|
+
: undefined,
|
|
157
|
+
notes: readStringArray(nested.notes),
|
|
158
|
+
retryAfterMs:
|
|
159
|
+
typeof nested.retryAfterMs === "number" && Number.isFinite(nested.retryAfterMs)
|
|
160
|
+
? nested.retryAfterMs
|
|
161
|
+
: undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const CREATE_MEET_FROM_BROWSER_SCRIPT = `async () => {
|
|
166
|
+
const meetUrlPattern = /^https:\\/\\/meet\\.google\\.com\\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i;
|
|
167
|
+
const text = (node) => (node?.innerText || node?.textContent || "").trim();
|
|
168
|
+
const current = () => location.href;
|
|
169
|
+
const notes = [];
|
|
170
|
+
const findButton = (pattern) =>
|
|
171
|
+
[...document.querySelectorAll("button")].find((button) => {
|
|
172
|
+
const label = [
|
|
173
|
+
button.getAttribute("aria-label"),
|
|
174
|
+
button.getAttribute("data-tooltip"),
|
|
175
|
+
text(button),
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join(" ");
|
|
179
|
+
return pattern.test(label) && !button.disabled;
|
|
180
|
+
});
|
|
181
|
+
const clickButton = (pattern, note) => {
|
|
182
|
+
const button = findButton(pattern);
|
|
183
|
+
if (!button) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
button.click();
|
|
187
|
+
notes.push(note);
|
|
188
|
+
return true;
|
|
189
|
+
};
|
|
190
|
+
if (!current().startsWith("https://meet.google.com/")) {
|
|
191
|
+
return {
|
|
192
|
+
manualActionReason: "google-login-required",
|
|
193
|
+
manualAction: "Sign in to Google in the AutoBot browser profile, then retry meeting creation.",
|
|
194
|
+
browserUrl: current(),
|
|
195
|
+
browserTitle: document.title,
|
|
196
|
+
notes,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const href = current();
|
|
200
|
+
if (meetUrlPattern.test(href)) {
|
|
201
|
+
return { meetingUri: href, browserUrl: href, browserTitle: document.title, notes };
|
|
202
|
+
}
|
|
203
|
+
const pageText = text(document.body);
|
|
204
|
+
if (clickButton(/\\buse microphone\\b/i, "Accepted Meet microphone prompt with browser automation.")) {
|
|
205
|
+
return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
|
|
206
|
+
}
|
|
207
|
+
if (
|
|
208
|
+
clickButton(
|
|
209
|
+
/continue without microphone/i,
|
|
210
|
+
"Continued through Meet microphone prompt with browser automation.",
|
|
211
|
+
)
|
|
212
|
+
) {
|
|
213
|
+
return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
|
|
214
|
+
}
|
|
215
|
+
if (/do you want people to hear you in the meeting/i.test(pageText)) {
|
|
216
|
+
return {
|
|
217
|
+
manualActionReason: "meet-audio-choice-required",
|
|
218
|
+
manualAction: "Meet is showing the microphone choice. Click Use microphone in the AutoBot browser profile, then retry meeting creation.",
|
|
219
|
+
browserUrl: href,
|
|
220
|
+
browserTitle: document.title,
|
|
221
|
+
notes,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (/allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera)/i.test(pageText)) {
|
|
225
|
+
return {
|
|
226
|
+
manualActionReason: "meet-permission-required",
|
|
227
|
+
manualAction: "Allow microphone/camera permissions for Meet in the AutoBot browser profile, then retry meeting creation.",
|
|
228
|
+
browserUrl: href,
|
|
229
|
+
browserTitle: document.title,
|
|
230
|
+
notes,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (/couldn't create|unable to create/i.test(pageText)) {
|
|
234
|
+
return {
|
|
235
|
+
manualAction: "Resolve the Google Meet page prompt in the AutoBot browser profile, then retry meeting creation.",
|
|
236
|
+
browserUrl: href,
|
|
237
|
+
browserTitle: document.title,
|
|
238
|
+
notes,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (location.hostname.toLowerCase() === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText)) {
|
|
242
|
+
return {
|
|
243
|
+
manualActionReason: "google-login-required",
|
|
244
|
+
manualAction: "Sign in to Google in the AutoBot browser profile, then retry meeting creation.",
|
|
245
|
+
browserUrl: href,
|
|
246
|
+
browserTitle: document.title,
|
|
247
|
+
notes,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
retryAfterMs: 500,
|
|
252
|
+
browserUrl: current(),
|
|
253
|
+
browserTitle: document.title,
|
|
254
|
+
notes,
|
|
255
|
+
};
|
|
256
|
+
}`;
|
|
257
|
+
|
|
258
|
+
export async function createMeetWithBrowserProxyOnNode(params: {
|
|
259
|
+
runtime: PluginRuntime;
|
|
260
|
+
config: GoogleMeetConfig;
|
|
261
|
+
}): Promise<GoogleMeetBrowserCreateResult> {
|
|
262
|
+
const nodeId = await resolveChromeNode({
|
|
263
|
+
runtime: params.runtime,
|
|
264
|
+
requestedNode: params.config.chromeNode.node,
|
|
265
|
+
});
|
|
266
|
+
const timeoutMs = Math.max(
|
|
267
|
+
GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS,
|
|
268
|
+
params.config.chrome.joinTimeoutMs,
|
|
269
|
+
);
|
|
270
|
+
const stepTimeoutMs = Math.min(timeoutMs, GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS);
|
|
271
|
+
let tab = await findGoogleMeetCreateTab({
|
|
272
|
+
runtime: params.runtime,
|
|
273
|
+
nodeId,
|
|
274
|
+
timeoutMs: stepTimeoutMs,
|
|
275
|
+
});
|
|
276
|
+
if (tab?.targetId) {
|
|
277
|
+
await focusBrowserTab({
|
|
278
|
+
runtime: params.runtime,
|
|
279
|
+
nodeId,
|
|
280
|
+
targetId: tab.targetId,
|
|
281
|
+
timeoutMs: stepTimeoutMs,
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
tab = readBrowserTab(
|
|
285
|
+
await callBrowserProxyOnNode({
|
|
286
|
+
runtime: params.runtime,
|
|
287
|
+
nodeId,
|
|
288
|
+
method: "POST",
|
|
289
|
+
path: "/tabs/open",
|
|
290
|
+
body: { url: GOOGLE_MEET_NEW_URL },
|
|
291
|
+
timeoutMs: stepTimeoutMs,
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const targetId = tab?.targetId;
|
|
296
|
+
if (!targetId) {
|
|
297
|
+
throw new Error("Browser fallback opened Google Meet but did not return a targetId.");
|
|
298
|
+
}
|
|
299
|
+
const notes = new Set<string>();
|
|
300
|
+
let lastResult: BrowserCreateStepResult | undefined;
|
|
301
|
+
let lastError: unknown;
|
|
302
|
+
const deadline = Date.now() + timeoutMs;
|
|
303
|
+
while (Date.now() <= deadline) {
|
|
304
|
+
try {
|
|
305
|
+
const evaluated = await callBrowserProxyOnNode({
|
|
306
|
+
runtime: params.runtime,
|
|
307
|
+
nodeId,
|
|
308
|
+
method: "POST",
|
|
309
|
+
path: "/act",
|
|
310
|
+
body: {
|
|
311
|
+
kind: "evaluate",
|
|
312
|
+
targetId,
|
|
313
|
+
fn: CREATE_MEET_FROM_BROWSER_SCRIPT,
|
|
314
|
+
},
|
|
315
|
+
timeoutMs: stepTimeoutMs,
|
|
316
|
+
});
|
|
317
|
+
const result = readBrowserCreateResult(evaluated);
|
|
318
|
+
lastResult = result;
|
|
319
|
+
for (const note of result.notes ?? []) {
|
|
320
|
+
notes.add(note);
|
|
321
|
+
}
|
|
322
|
+
if (result.meetingUri) {
|
|
323
|
+
return {
|
|
324
|
+
source: "browser",
|
|
325
|
+
nodeId,
|
|
326
|
+
targetId,
|
|
327
|
+
meetingUri: result.meetingUri,
|
|
328
|
+
browserUrl: result.browserUrl,
|
|
329
|
+
browserTitle: result.browserTitle,
|
|
330
|
+
notes: [...notes],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (result.manualAction) {
|
|
334
|
+
throw new GoogleMeetBrowserManualActionError({
|
|
335
|
+
manualActionRequired: true,
|
|
336
|
+
manualActionReason: result.manualActionReason,
|
|
337
|
+
manualActionMessage: result.manualAction,
|
|
338
|
+
browser: {
|
|
339
|
+
nodeId,
|
|
340
|
+
targetId,
|
|
341
|
+
browserUrl: result.browserUrl,
|
|
342
|
+
browserTitle: result.browserTitle,
|
|
343
|
+
notes: [...notes],
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
await sleep(result.retryAfterMs ?? GOOGLE_MEET_BROWSER_POLL_MS);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
lastError = error;
|
|
350
|
+
if (!isBrowserNavigationInterruption(error)) {
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
await sleep(GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
throw new Error(
|
|
357
|
+
lastResult?.manualAction ??
|
|
358
|
+
`Google Meet did not return a meeting URL from the browser create flow before timeout.${
|
|
359
|
+
lastError
|
|
360
|
+
? ` Last browser automation error: ${formatBrowserAutomationError(lastError)}`
|
|
361
|
+
: ""
|
|
362
|
+
}`,
|
|
363
|
+
);
|
|
364
|
+
}
|