@ai-matrx/meet 0.8.1 → 0.10.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/CHANGELOG.md +40 -0
- package/dist/{collab-Bss-_Jfp.d.cts → collab-C2R-kQnV.d.cts} +24 -13
- package/dist/{collab-Bss-_Jfp.d.ts → collab-C2R-kQnV.d.ts} +24 -13
- package/dist/{components-BmgstFx_.d.ts → components-CMtSekZS.d.ts} +1 -1
- package/dist/{components-BOUmOiPj.d.cts → components-DKHfkEVB.d.cts} +1 -1
- package/dist/core.cjs +452 -154
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +132 -41
- package/dist/core.d.ts +132 -41
- package/dist/core.js +452 -154
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +452 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +452 -154
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +550 -184
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +6 -5
- package/dist/react.d.ts +6 -5
- package/dist/react.js +550 -184
- package/dist/react.js.map +1 -1
- package/dist/skins/meet.cjs +141 -34
- package/dist/skins/meet.cjs.map +1 -1
- package/dist/skins/meet.d.cts +2 -2
- package/dist/skins/meet.d.ts +2 -2
- package/dist/skins/meet.js +141 -34
- package/dist/skins/meet.js.map +1 -1
- package/dist/styles.css +8 -0
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -7,11 +7,18 @@ var MeetError = class extends Error {
|
|
|
7
7
|
/** What to DO. Rendered to the user for the device codes. */
|
|
8
8
|
remedy;
|
|
9
9
|
cause;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
|
|
12
|
+
* `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
|
|
13
|
+
* renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
|
|
14
|
+
*/
|
|
15
|
+
reason;
|
|
16
|
+
constructor(code, message, remedy, cause, reason) {
|
|
11
17
|
super(message);
|
|
12
18
|
this.name = "MeetError";
|
|
13
19
|
this.code = code;
|
|
14
20
|
this.remedy = remedy;
|
|
21
|
+
this.reason = reason ?? null;
|
|
15
22
|
if (cause !== void 0) this.cause = cause;
|
|
16
23
|
}
|
|
17
24
|
/** True when retrying after the host re-establishes a session is the fix. */
|
|
@@ -229,6 +236,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
|
|
|
229
236
|
}
|
|
230
237
|
};
|
|
231
238
|
}
|
|
239
|
+
function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
|
|
240
|
+
const raw = storage.read(roomName);
|
|
241
|
+
if (raw === null) return null;
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(raw);
|
|
244
|
+
if (parsed.standing !== "guest") return null;
|
|
245
|
+
return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
232
250
|
var MEET_ROUTES = {
|
|
233
251
|
token: "/api/v1/meet/token",
|
|
234
252
|
recordingStart: "/api/v1/meet/recording/start",
|
|
@@ -242,7 +260,7 @@ var MEET_ROUTES = {
|
|
|
242
260
|
/**
|
|
243
261
|
* THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
|
|
244
262
|
* authorized by the room token this device already holds, carried in the
|
|
245
|
-
* `x-meet-
|
|
263
|
+
* `x-meet-pass` header — never a query parameter, because a bearer
|
|
246
264
|
* credential in a URL lands in access logs, referrers and history.
|
|
247
265
|
*/
|
|
248
266
|
record: "/api/v1/meet/record",
|
|
@@ -250,7 +268,7 @@ var MEET_ROUTES = {
|
|
|
250
268
|
recordAccess: "/api/v1/meet/record/access",
|
|
251
269
|
/**
|
|
252
270
|
* KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
|
|
253
|
-
* their kept pass in `x-meet-
|
|
271
|
+
* their kept pass in `x-meet-pass` with their new session, and the
|
|
254
272
|
* meeting they attended becomes theirs to read. Idempotent; never a grant.
|
|
255
273
|
*/
|
|
256
274
|
claim: "/api/v1/meet/claim",
|
|
@@ -272,7 +290,7 @@ var MEET_ROUTES = {
|
|
|
272
290
|
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
273
291
|
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
274
292
|
* Decided by the server from the meeting itself; a guest's pass rides
|
|
275
|
-
* `x-meet-
|
|
293
|
+
* `x-meet-pass`.
|
|
276
294
|
*/
|
|
277
295
|
moderate: "/api/v1/meet/moderate",
|
|
278
296
|
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
@@ -283,12 +301,26 @@ var MEET_ROUTES = {
|
|
|
283
301
|
*/
|
|
284
302
|
collab: "/api/v1/meet/collab",
|
|
285
303
|
/** Meet wave 5: translate finished caption lines into this viewer's language. */
|
|
286
|
-
captionsTranslate: "/api/v1/meet/captions/translate"
|
|
304
|
+
captionsTranslate: "/api/v1/meet/captions/translate",
|
|
305
|
+
/**
|
|
306
|
+
* S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
|
|
307
|
+
* person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
|
|
308
|
+
* phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
|
|
309
|
+
*/
|
|
310
|
+
state: "/api/v1/meet/state",
|
|
311
|
+
/** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
|
|
312
|
+
leave: "/api/v1/meet/leave",
|
|
313
|
+
/** S4: a pass holder (guests included) saw the recording notice, for this run. */
|
|
314
|
+
consent: "/api/v1/meet/consent"
|
|
287
315
|
};
|
|
288
316
|
function meetingPath(template, meetingId) {
|
|
289
317
|
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
290
318
|
}
|
|
291
|
-
var
|
|
319
|
+
var MEETING_PASS_HEADER = "x-meet-pass";
|
|
320
|
+
function meetingPassHeaders(token) {
|
|
321
|
+
const pass = token?.meetingPass ?? null;
|
|
322
|
+
return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
|
|
323
|
+
}
|
|
292
324
|
var REFRESH_RATIO = 0.8;
|
|
293
325
|
function readString(source, key) {
|
|
294
326
|
const value = source[key];
|
|
@@ -298,7 +330,12 @@ function readBoolean(source, key, fallback) {
|
|
|
298
330
|
const value = source[key];
|
|
299
331
|
return typeof value === "boolean" ? value : fallback;
|
|
300
332
|
}
|
|
301
|
-
var
|
|
333
|
+
var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
|
|
334
|
+
function readStanding(source, key) {
|
|
335
|
+
const value = source[key];
|
|
336
|
+
return TOKEN_STANDINGS.includes(value) ? value : null;
|
|
337
|
+
}
|
|
338
|
+
var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
302
339
|
function readRole(source, key) {
|
|
303
340
|
const value = source[key];
|
|
304
341
|
return TOKEN_ROLES.includes(value) ? value : null;
|
|
@@ -341,8 +378,15 @@ function parseRoomToken(body, roomName) {
|
|
|
341
378
|
// Deriving the role from what DID arrive keeps a host a host against a
|
|
342
379
|
// server that has not landed MRI-A11 — and never invents a host from
|
|
343
380
|
// silence, because both fallbacks below grant nothing.
|
|
344
|
-
|
|
381
|
+
// A word this package does not know grants nothing: `participant`, never a host.
|
|
382
|
+
role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
|
|
383
|
+
standing: readStanding(payload, "standing"),
|
|
384
|
+
meetingPass: readString(payload, "meeting_pass"),
|
|
385
|
+
meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
|
|
386
|
+
sessionId: readString(payload, "session_id"),
|
|
345
387
|
lobby: readBoolean(payload, "lobby", false),
|
|
388
|
+
admissionState: readString(payload, "admission_state") ?? void 0,
|
|
389
|
+
pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
|
|
346
390
|
breakoutRoomId: readString(payload, "breakout_room_id") ?? null
|
|
347
391
|
};
|
|
348
392
|
}
|
|
@@ -352,7 +396,7 @@ function createTokenClient(options) {
|
|
|
352
396
|
const storage = options.storage;
|
|
353
397
|
function remember(token) {
|
|
354
398
|
if (storage === void 0) return;
|
|
355
|
-
if (
|
|
399
|
+
if (token.standing !== "guest") return;
|
|
356
400
|
storage.write(token.roomName, JSON.stringify(token));
|
|
357
401
|
}
|
|
358
402
|
function recall(roomName) {
|
|
@@ -410,22 +454,28 @@ function createTokenClient(options) {
|
|
|
410
454
|
throw new MeetError(
|
|
411
455
|
"forbidden",
|
|
412
456
|
said?.message ?? "You can't join this meeting.",
|
|
413
|
-
said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
|
|
457
|
+
said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
|
|
458
|
+
void 0,
|
|
459
|
+
said?.reason ?? null
|
|
414
460
|
);
|
|
415
461
|
}
|
|
416
462
|
if (response.status === 404) {
|
|
417
463
|
throw new MeetError(
|
|
418
464
|
"not-found",
|
|
419
465
|
"meet.token: no such meeting",
|
|
420
|
-
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
|
|
466
|
+
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
|
|
467
|
+
void 0,
|
|
468
|
+
"not_found"
|
|
421
469
|
);
|
|
422
470
|
}
|
|
423
471
|
if (response.status === 410) {
|
|
424
|
-
const
|
|
472
|
+
const said = await serverSentence(response);
|
|
425
473
|
throw new MeetError(
|
|
426
474
|
"cancelled",
|
|
427
|
-
|
|
428
|
-
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
|
|
475
|
+
said?.message ?? "This meeting was cancelled.",
|
|
476
|
+
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
|
|
477
|
+
void 0,
|
|
478
|
+
said?.reason ?? "cancelled"
|
|
429
479
|
);
|
|
430
480
|
}
|
|
431
481
|
if (response.status === 423) {
|
|
@@ -433,7 +483,9 @@ function createTokenClient(options) {
|
|
|
433
483
|
throw new MeetError(
|
|
434
484
|
"room-locked",
|
|
435
485
|
said?.message ?? "The meeting is locked.",
|
|
436
|
-
said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
|
|
486
|
+
said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
|
|
487
|
+
void 0,
|
|
488
|
+
said?.reason ?? "locked"
|
|
437
489
|
);
|
|
438
490
|
}
|
|
439
491
|
if (response.status === 503) {
|
|
@@ -502,6 +554,10 @@ function placeholder(roomName) {
|
|
|
502
554
|
canRecord: false,
|
|
503
555
|
isHost: false,
|
|
504
556
|
role: "participant",
|
|
557
|
+
standing: null,
|
|
558
|
+
meetingPass: null,
|
|
559
|
+
meetingPassExpiresAt: null,
|
|
560
|
+
sessionId: null,
|
|
505
561
|
lobby: false,
|
|
506
562
|
breakoutRoomId: null
|
|
507
563
|
};
|
|
@@ -514,7 +570,8 @@ async function serverSentence(response) {
|
|
|
514
570
|
const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
|
|
515
571
|
if (message === null) return null;
|
|
516
572
|
const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
|
|
517
|
-
|
|
573
|
+
const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
|
|
574
|
+
return { message, remedy, reason };
|
|
518
575
|
} catch {
|
|
519
576
|
return null;
|
|
520
577
|
}
|
|
@@ -703,7 +760,7 @@ function createMeetAi(options) {
|
|
|
703
760
|
operation: "meet.ai.ask",
|
|
704
761
|
// A GUEST MAY ASK. The server decides whether it can answer.
|
|
705
762
|
requireAuth: false,
|
|
706
|
-
...roomToken !== null ? { headers: { [
|
|
763
|
+
...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
|
|
707
764
|
...signal !== void 0 ? { signal } : {}
|
|
708
765
|
});
|
|
709
766
|
if (!response.ok) {
|
|
@@ -942,14 +999,15 @@ var MEET_EVENTS = {
|
|
|
942
999
|
cancel: "mx.call.cancel",
|
|
943
1000
|
/** You answered (or declined) on another device. Stop ringing here. */
|
|
944
1001
|
settled: "mx.call.settled",
|
|
945
|
-
/**
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1002
|
+
/**
|
|
1003
|
+
* 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
|
|
1004
|
+
* read again". Nothing a person must see rides it: a waiting person learns admitted /
|
|
1005
|
+
* denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
|
|
1006
|
+
* every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
|
|
1007
|
+
* `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
|
|
1008
|
+
* server publishes the same event on the same topic (aidream `admission.room_topic`).
|
|
1009
|
+
*/
|
|
1010
|
+
stateChanged: "mx.state.changed"
|
|
953
1011
|
};
|
|
954
1012
|
|
|
955
1013
|
// src/core/types.ts
|
|
@@ -963,14 +1021,19 @@ function isGuest(identity) {
|
|
|
963
1021
|
return identity.userId === null;
|
|
964
1022
|
}
|
|
965
1023
|
var ROLE_RANK = {
|
|
966
|
-
host:
|
|
967
|
-
cohost:
|
|
1024
|
+
host: 4,
|
|
1025
|
+
cohost: 3,
|
|
1026
|
+
presenter: 2,
|
|
968
1027
|
participant: 1,
|
|
969
|
-
|
|
1028
|
+
viewer: 0
|
|
970
1029
|
};
|
|
1030
|
+
function isLinkFollower(standing) {
|
|
1031
|
+
return standing === "guest" || standing === "outsider";
|
|
1032
|
+
}
|
|
971
1033
|
function isHostRole(role) {
|
|
972
1034
|
return role === "host" || role === "cohost";
|
|
973
1035
|
}
|
|
1036
|
+
var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
|
|
974
1037
|
|
|
975
1038
|
// src/core/calls.ts
|
|
976
1039
|
var TRANSITIONS = {
|
|
@@ -1520,15 +1583,15 @@ var CAPTION_LANGUAGES = [
|
|
|
1520
1583
|
{ tag: "id", name: "Indonesian" },
|
|
1521
1584
|
{ tag: "sv", name: "Swedish" }
|
|
1522
1585
|
];
|
|
1523
|
-
async function loadMeetingActivities(api, meetingId,
|
|
1586
|
+
async function loadMeetingActivities(api, meetingId, meetingPass = null) {
|
|
1524
1587
|
let response;
|
|
1525
1588
|
try {
|
|
1526
1589
|
response = await api.request({
|
|
1527
1590
|
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
1528
1591
|
method: "GET",
|
|
1529
1592
|
operation: "meet.loadMeetingActivities",
|
|
1530
|
-
requireAuth:
|
|
1531
|
-
...
|
|
1593
|
+
requireAuth: meetingPass === null,
|
|
1594
|
+
...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
|
|
1532
1595
|
});
|
|
1533
1596
|
} catch (error) {
|
|
1534
1597
|
return {
|
|
@@ -1583,9 +1646,15 @@ var RPCS = {
|
|
|
1583
1646
|
getOrCreateMeeting: "meet_get_or_create_meeting",
|
|
1584
1647
|
/** Resolve a durable public link, before/during/after its scheduled time. */
|
|
1585
1648
|
meetingBySlug: "meet_meeting_by_slug",
|
|
1586
|
-
|
|
1649
|
+
/*
|
|
1650
|
+
* ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
|
|
1651
|
+
* sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
|
|
1652
|
+
*/
|
|
1653
|
+
admit: "meet_admit",
|
|
1654
|
+
deny: "meet_deny",
|
|
1655
|
+
undeny: "meet_undeny",
|
|
1656
|
+
admitAll: "meet_admit_all",
|
|
1587
1657
|
setLock: "meet_set_lock",
|
|
1588
|
-
endMeeting: "meet_end_meeting",
|
|
1589
1658
|
recordConsent: "meet_record_consent",
|
|
1590
1659
|
/*
|
|
1591
1660
|
* ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
|
|
@@ -1627,13 +1696,13 @@ var RPCS = {
|
|
|
1627
1696
|
/** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
|
|
1628
1697
|
search: "meet_search"
|
|
1629
1698
|
};
|
|
1630
|
-
var VOID_RPCS = [RPCS.
|
|
1699
|
+
var VOID_RPCS = [RPCS.recordConsent];
|
|
1631
1700
|
var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
|
|
1632
1701
|
var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
|
|
1633
1702
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1634
1703
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1635
1704
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
1636
|
-
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
|
|
1705
|
+
var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions";
|
|
1637
1706
|
function projectStays(value) {
|
|
1638
1707
|
if (!Array.isArray(value)) return [];
|
|
1639
1708
|
const stays = [];
|
|
@@ -1999,8 +2068,17 @@ function createMeetRepository(options) {
|
|
|
1999
2068
|
const operation = "meet.projectAttendee";
|
|
2000
2069
|
const role = text(row, "role");
|
|
2001
2070
|
const admission = text(row, "admission_state");
|
|
2002
|
-
const known = ["host", "cohost", "participant", "
|
|
2003
|
-
const states = [
|
|
2071
|
+
const known = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
2072
|
+
const states = [
|
|
2073
|
+
"waiting_host",
|
|
2074
|
+
"knocking",
|
|
2075
|
+
"admitted",
|
|
2076
|
+
"denied",
|
|
2077
|
+
"expired",
|
|
2078
|
+
"withdrawn",
|
|
2079
|
+
"removed",
|
|
2080
|
+
"ended"
|
|
2081
|
+
];
|
|
2004
2082
|
const userId = text(row, "participant_user_id");
|
|
2005
2083
|
const identity = requireText(row, "identity", operation);
|
|
2006
2084
|
return {
|
|
@@ -2014,6 +2092,8 @@ function createMeetRepository(options) {
|
|
|
2014
2092
|
userId: userId === null ? null : asUserId(userId),
|
|
2015
2093
|
role: known.includes(role) ? role : "participant",
|
|
2016
2094
|
admissionState: states.includes(admission) ? admission : "admitted",
|
|
2095
|
+
knockedAt: text(row, "knocked_at"),
|
|
2096
|
+
knockExpiresAt: text(row, "knock_expires_at"),
|
|
2017
2097
|
isAgent: bool(row, "is_agent", false),
|
|
2018
2098
|
joinedAt: text(row, "joined_at"),
|
|
2019
2099
|
leftAt: text(row, "left_at"),
|
|
@@ -2292,16 +2372,26 @@ function createMeetRepository(options) {
|
|
|
2292
2372
|
hasMore: rows.length > limit
|
|
2293
2373
|
};
|
|
2294
2374
|
},
|
|
2295
|
-
async
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2375
|
+
async decideAdmission({ meetingId, identity, decision, byUserId }) {
|
|
2376
|
+
const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
|
|
2377
|
+
const row = await guard(
|
|
2378
|
+
`meet.${decision}`,
|
|
2379
|
+
() => db.rpc(fn, {
|
|
2299
2380
|
p_meeting_id: meetingId,
|
|
2300
2381
|
p_identity: identity,
|
|
2301
|
-
p_admitted: admitted,
|
|
2302
2382
|
p_by_user_id: byUserId
|
|
2303
2383
|
})
|
|
2304
2384
|
);
|
|
2385
|
+
return projectAttendee(row);
|
|
2386
|
+
},
|
|
2387
|
+
async admitAll(meetingId, byUserId) {
|
|
2388
|
+
const response = await db.rpc(RPCS.admitAll, {
|
|
2389
|
+
p_meeting_id: meetingId,
|
|
2390
|
+
p_by_user_id: byUserId
|
|
2391
|
+
});
|
|
2392
|
+
if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
|
|
2393
|
+
const rows = Array.isArray(response.data) ? response.data : [];
|
|
2394
|
+
return rows.map(projectAttendee);
|
|
2305
2395
|
},
|
|
2306
2396
|
async setLock(meetingId, locked, byUserId) {
|
|
2307
2397
|
const row = await guard(
|
|
@@ -2314,16 +2404,6 @@ function createMeetRepository(options) {
|
|
|
2314
2404
|
);
|
|
2315
2405
|
return projectMeeting(row);
|
|
2316
2406
|
},
|
|
2317
|
-
async endMeeting(meetingId, byUserId) {
|
|
2318
|
-
const row = await guard(
|
|
2319
|
-
"meet.endMeeting",
|
|
2320
|
-
() => db.rpc(RPCS.endMeeting, {
|
|
2321
|
-
p_meeting_id: meetingId,
|
|
2322
|
-
p_by_user_id: byUserId
|
|
2323
|
-
})
|
|
2324
|
-
);
|
|
2325
|
-
return projectMeeting(row);
|
|
2326
|
-
},
|
|
2327
2407
|
async transcript(meetingId, limit = 1e3) {
|
|
2328
2408
|
const response = await db.from(TABLES.transcripts).select(SEGMENT_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: true }).order("id", { ascending: true }).limit(limit);
|
|
2329
2409
|
if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
|
|
@@ -2652,12 +2732,14 @@ var DEFAULT_EFFECTS = {
|
|
|
2652
2732
|
};
|
|
2653
2733
|
var NO_GRANTS = {
|
|
2654
2734
|
role: "participant",
|
|
2735
|
+
standing: null,
|
|
2655
2736
|
canRecord: false,
|
|
2656
2737
|
canPublish: false,
|
|
2657
2738
|
canSubscribe: false
|
|
2658
2739
|
};
|
|
2659
2740
|
var EMPTY = {
|
|
2660
2741
|
phase: "idle",
|
|
2742
|
+
waiting: null,
|
|
2661
2743
|
meeting: null,
|
|
2662
2744
|
localIdentity: null,
|
|
2663
2745
|
grants: NO_GRANTS,
|
|
@@ -2698,6 +2780,11 @@ function effectiveRole(snapshot) {
|
|
|
2698
2780
|
const fromToken = snapshot.grants.role;
|
|
2699
2781
|
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
2700
2782
|
}
|
|
2783
|
+
function effectiveStanding(snapshot) {
|
|
2784
|
+
if (snapshot.grants.standing != null) return snapshot.grants.standing;
|
|
2785
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
2786
|
+
return me?.standing ?? null;
|
|
2787
|
+
}
|
|
2701
2788
|
var REACTION_TTL_MS = 5e3;
|
|
2702
2789
|
var CHAT_LIMIT = 200;
|
|
2703
2790
|
function createMeetStore(initial = {}) {
|
|
@@ -3190,7 +3277,7 @@ function applyModeration(store, payload, now) {
|
|
|
3190
3277
|
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3191
3278
|
}
|
|
3192
3279
|
}
|
|
3193
|
-
var ROLES = ["host", "cohost", "participant", "
|
|
3280
|
+
var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
3194
3281
|
|
|
3195
3282
|
// src/core/devices.ts
|
|
3196
3283
|
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
@@ -3930,19 +4017,25 @@ function roleOf(participants, identity) {
|
|
|
3930
4017
|
return participants.find((p) => p.identity === identity)?.role ?? "participant";
|
|
3931
4018
|
}
|
|
3932
4019
|
var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
|
|
3933
|
-
function lobbyEntryFrom(attendee) {
|
|
3934
|
-
if (attendee.admissionState
|
|
4020
|
+
function lobbyEntryFrom(attendee, now = Date.now()) {
|
|
4021
|
+
if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
|
|
4022
|
+
const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
|
|
4023
|
+
if (!Number.isNaN(expires) && expires <= now) return null;
|
|
3935
4024
|
return {
|
|
3936
4025
|
identity: attendee.identity,
|
|
3937
4026
|
displayName: attendee.displayName,
|
|
3938
4027
|
avatarUrl: attendee.avatarUrl,
|
|
3939
4028
|
userId: attendee.userId,
|
|
3940
4029
|
state: "knocking",
|
|
3941
|
-
knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4030
|
+
knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4031
|
+
knockExpiresAt: attendee.knockExpiresAt ?? null,
|
|
3942
4032
|
decidedAt: null,
|
|
3943
4033
|
decidedBy: null
|
|
3944
4034
|
};
|
|
3945
4035
|
}
|
|
4036
|
+
function decidedState(state) {
|
|
4037
|
+
return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
|
|
4038
|
+
}
|
|
3946
4039
|
function createMeetingSession(options) {
|
|
3947
4040
|
const { manager, repository, store, identity } = options;
|
|
3948
4041
|
let channel = null;
|
|
@@ -3966,11 +4059,6 @@ function createMeetingSession(options) {
|
|
|
3966
4059
|
}
|
|
3967
4060
|
return identity.userId;
|
|
3968
4061
|
}
|
|
3969
|
-
function knockIdentity() {
|
|
3970
|
-
return asParticipantIdentity(
|
|
3971
|
-
identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
|
|
3972
|
-
);
|
|
3973
|
-
}
|
|
3974
4062
|
function requireMeeting(operation) {
|
|
3975
4063
|
const meeting = store.snapshot().meeting;
|
|
3976
4064
|
if (meeting === null) {
|
|
@@ -3982,6 +4070,191 @@ function createMeetingSession(options) {
|
|
|
3982
4070
|
}
|
|
3983
4071
|
return meeting;
|
|
3984
4072
|
}
|
|
4073
|
+
let polling = false;
|
|
4074
|
+
let pollTimer = null;
|
|
4075
|
+
let reading = null;
|
|
4076
|
+
let admittedOnce = false;
|
|
4077
|
+
let unlisten = null;
|
|
4078
|
+
function report(level, message, remedy) {
|
|
4079
|
+
options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
|
|
4080
|
+
}
|
|
4081
|
+
function pollMs() {
|
|
4082
|
+
const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
|
|
4083
|
+
return Math.max(1, Math.min(60, seconds)) * 1e3;
|
|
4084
|
+
}
|
|
4085
|
+
function schedule(ms2) {
|
|
4086
|
+
if (pollTimer !== null) clearTimeout(pollTimer);
|
|
4087
|
+
if (!polling) return;
|
|
4088
|
+
pollTimer = setTimeout(() => {
|
|
4089
|
+
pollTimer = null;
|
|
4090
|
+
void readState2().finally(() => schedule(pollMs()));
|
|
4091
|
+
}, ms2);
|
|
4092
|
+
}
|
|
4093
|
+
function listen() {
|
|
4094
|
+
if (unlisten !== null || typeof window === "undefined") return;
|
|
4095
|
+
const nudge = () => {
|
|
4096
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
4097
|
+
void readState2();
|
|
4098
|
+
};
|
|
4099
|
+
const goodbye = () => {
|
|
4100
|
+
const meeting = store.snapshot().meeting;
|
|
4101
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4102
|
+
const waiting = store.snapshot().waiting;
|
|
4103
|
+
if (meeting === null || pass === null || waiting === null) return;
|
|
4104
|
+
if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
|
|
4105
|
+
try {
|
|
4106
|
+
navigator.sendBeacon?.(
|
|
4107
|
+
options.api.url(MEET_ROUTES.leave),
|
|
4108
|
+
new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
|
|
4109
|
+
type: "application/json"
|
|
4110
|
+
})
|
|
4111
|
+
);
|
|
4112
|
+
} catch {
|
|
4113
|
+
}
|
|
4114
|
+
};
|
|
4115
|
+
window.addEventListener("online", nudge);
|
|
4116
|
+
window.addEventListener("pagehide", goodbye);
|
|
4117
|
+
document.addEventListener("visibilitychange", nudge);
|
|
4118
|
+
unlisten = () => {
|
|
4119
|
+
window.removeEventListener("online", nudge);
|
|
4120
|
+
window.removeEventListener("pagehide", goodbye);
|
|
4121
|
+
document.removeEventListener("visibilitychange", nudge);
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
function stopWaiting() {
|
|
4125
|
+
polling = false;
|
|
4126
|
+
if (pollTimer !== null) clearTimeout(pollTimer);
|
|
4127
|
+
pollTimer = null;
|
|
4128
|
+
unlisten?.();
|
|
4129
|
+
unlisten = null;
|
|
4130
|
+
}
|
|
4131
|
+
function patchWaiting(next) {
|
|
4132
|
+
const current = store.snapshot().waiting;
|
|
4133
|
+
const base = current ?? {
|
|
4134
|
+
admission: "knocking",
|
|
4135
|
+
admittersPresent: null,
|
|
4136
|
+
reknockAllowed: true,
|
|
4137
|
+
knockExpiresAt: null,
|
|
4138
|
+
pollSeconds: 3,
|
|
4139
|
+
liveVersion: 0
|
|
4140
|
+
};
|
|
4141
|
+
store.patch({ waiting: { ...base, ...next } });
|
|
4142
|
+
}
|
|
4143
|
+
function apply(read) {
|
|
4144
|
+
const snapshot = store.snapshot();
|
|
4145
|
+
if (snapshot.phase !== "lobby") return;
|
|
4146
|
+
const admission = read.admission_state ?? null;
|
|
4147
|
+
const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
|
|
4148
|
+
if (admission === "admitted" && !ended) {
|
|
4149
|
+
if (admittedOnce) return;
|
|
4150
|
+
admittedOnce = true;
|
|
4151
|
+
stopWaiting();
|
|
4152
|
+
options.onAdmitted?.();
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
if (ended) {
|
|
4156
|
+
stopWaiting();
|
|
4157
|
+
patchWaiting({ admission: "ended" });
|
|
4158
|
+
const meeting = snapshot.meeting;
|
|
4159
|
+
if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
|
|
4160
|
+
store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
4161
|
+
}
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if (admission === null) return;
|
|
4165
|
+
patchWaiting({
|
|
4166
|
+
admission,
|
|
4167
|
+
admittersPresent: read.admitters_present ?? null,
|
|
4168
|
+
reknockAllowed: read.reknock_allowed ?? true,
|
|
4169
|
+
knockExpiresAt: read.knock_expires_at ?? null,
|
|
4170
|
+
pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
|
|
4171
|
+
liveVersion: read.live_version ?? 0
|
|
4172
|
+
});
|
|
4173
|
+
if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
|
|
4174
|
+
stopWaiting();
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
async function readState2() {
|
|
4178
|
+
if (reading !== null) return reading;
|
|
4179
|
+
const meeting = store.snapshot().meeting;
|
|
4180
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4181
|
+
if (meeting === null || pass === null) return;
|
|
4182
|
+
reading = (async () => {
|
|
4183
|
+
try {
|
|
4184
|
+
const response = await options.api.request({
|
|
4185
|
+
path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
|
|
4186
|
+
method: "GET",
|
|
4187
|
+
requireAuth: false,
|
|
4188
|
+
operation: "meet.state",
|
|
4189
|
+
headers: { [MEETING_PASS_HEADER]: pass }
|
|
4190
|
+
});
|
|
4191
|
+
if (!response.ok) {
|
|
4192
|
+
let body = null;
|
|
4193
|
+
try {
|
|
4194
|
+
body = await response.json();
|
|
4195
|
+
} catch {
|
|
4196
|
+
body = null;
|
|
4197
|
+
}
|
|
4198
|
+
const refusal2 = readServerRefusal(body);
|
|
4199
|
+
report(
|
|
4200
|
+
"warn",
|
|
4201
|
+
refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
|
|
4202
|
+
refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
|
|
4203
|
+
);
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
apply(await response.json());
|
|
4207
|
+
} catch (error) {
|
|
4208
|
+
report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
|
|
4209
|
+
} finally {
|
|
4210
|
+
reading = null;
|
|
4211
|
+
}
|
|
4212
|
+
})();
|
|
4213
|
+
return reading;
|
|
4214
|
+
}
|
|
4215
|
+
async function postLeave(intent) {
|
|
4216
|
+
const meeting = requireMeeting(`meet.${intent}`);
|
|
4217
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4218
|
+
const response = await options.api.request({
|
|
4219
|
+
path: MEET_ROUTES.leave,
|
|
4220
|
+
method: "POST",
|
|
4221
|
+
requireAuth: false,
|
|
4222
|
+
operation: `meet.${intent}`,
|
|
4223
|
+
body: { meeting_id: meeting.id, intent },
|
|
4224
|
+
...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
|
|
4225
|
+
});
|
|
4226
|
+
if (!response.ok) {
|
|
4227
|
+
let body = null;
|
|
4228
|
+
try {
|
|
4229
|
+
body = await response.json();
|
|
4230
|
+
} catch {
|
|
4231
|
+
body = null;
|
|
4232
|
+
}
|
|
4233
|
+
const refusal2 = readServerRefusal(body);
|
|
4234
|
+
throw new MeetError(
|
|
4235
|
+
"transport",
|
|
4236
|
+
refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
|
|
4237
|
+
refusal2?.remedy ?? "You are still in the waiting room. Try again."
|
|
4238
|
+
);
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
function hint() {
|
|
4242
|
+
channel?.send(MEET_EVENTS.stateChanged, { v: null });
|
|
4243
|
+
}
|
|
4244
|
+
async function refreshQueue() {
|
|
4245
|
+
const meeting = store.snapshot().meeting;
|
|
4246
|
+
if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
|
|
4247
|
+
try {
|
|
4248
|
+
const attendees = await repository.attendees(meeting.id);
|
|
4249
|
+
store.learn(attendees);
|
|
4250
|
+
for (const attendee of attendees) {
|
|
4251
|
+
const entry = lobbyEntryFrom(attendee);
|
|
4252
|
+
if (entry !== null) store.upsertLobby(entry);
|
|
4253
|
+
else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4254
|
+
}
|
|
4255
|
+
} catch {
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
3985
4258
|
return {
|
|
3986
4259
|
watch(roomName) {
|
|
3987
4260
|
channel?.close();
|
|
@@ -3989,58 +4262,22 @@ function createMeetingSession(options) {
|
|
|
3989
4262
|
topic: roomTopic(roomName),
|
|
3990
4263
|
broadcast: [
|
|
3991
4264
|
{
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
{
|
|
3998
|
-
event: MEET_EVENTS.admission,
|
|
3999
|
-
onMessage: (message) => {
|
|
4000
|
-
const verdict = message.data;
|
|
4001
|
-
const decided = asParticipantIdentity(verdict.identity);
|
|
4002
|
-
store.setAdmission(
|
|
4003
|
-
decided,
|
|
4004
|
-
verdict.admitted ? "admitted" : "denied",
|
|
4005
|
-
verdict.by === null ? null : verdict.by
|
|
4006
|
-
);
|
|
4007
|
-
if (decided === knockIdentity()) {
|
|
4008
|
-
if (verdict.admitted) options.onAdmitted?.();
|
|
4009
|
-
else options.onDenied?.();
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
4012
|
-
},
|
|
4013
|
-
{
|
|
4014
|
-
event: MEET_EVENTS.lock,
|
|
4015
|
-
onMessage: (message) => {
|
|
4016
|
-
const { locked } = message.data;
|
|
4017
|
-
const meeting = store.snapshot().meeting;
|
|
4018
|
-
store.patch({
|
|
4019
|
-
locked,
|
|
4020
|
-
...meeting !== null ? { meeting: { ...meeting, locked } } : {}
|
|
4021
|
-
});
|
|
4022
|
-
}
|
|
4023
|
-
},
|
|
4024
|
-
{
|
|
4025
|
-
event: MEET_EVENTS.recording,
|
|
4026
|
-
onMessage: (message) => {
|
|
4027
|
-
const payload = message.data;
|
|
4028
|
-
store.setRecording(
|
|
4029
|
-
payload.state,
|
|
4030
|
-
payload.by === null ? null : payload.by,
|
|
4031
|
-
"room"
|
|
4032
|
-
);
|
|
4265
|
+
// 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
|
|
4266
|
+
event: MEET_EVENTS.stateChanged,
|
|
4267
|
+
onMessage: () => {
|
|
4268
|
+
if (store.snapshot().phase === "lobby") void readState2();
|
|
4269
|
+
void refreshQueue();
|
|
4033
4270
|
}
|
|
4034
4271
|
}
|
|
4035
4272
|
],
|
|
4036
|
-
// THE BACKFILL DOOR. A
|
|
4037
|
-
//
|
|
4038
|
-
// re-announce on their own timer.
|
|
4273
|
+
// THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
|
|
4274
|
+
// row, and a waiting tab re-reads its own state.
|
|
4039
4275
|
onBackfill: async () => {
|
|
4040
4276
|
const meeting = store.snapshot().meeting;
|
|
4041
4277
|
if (meeting === null) return;
|
|
4042
4278
|
const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
|
|
4043
4279
|
store.patch({ meeting: fresh, locked: fresh.locked });
|
|
4280
|
+
if (store.snapshot().phase === "lobby") void readState2();
|
|
4044
4281
|
}
|
|
4045
4282
|
});
|
|
4046
4283
|
},
|
|
@@ -4101,7 +4338,7 @@ function createMeetingSession(options) {
|
|
|
4101
4338
|
store.upsertLobby(entry);
|
|
4102
4339
|
return;
|
|
4103
4340
|
}
|
|
4104
|
-
store.setAdmission(attendee.identity, attendee.admissionState, null);
|
|
4341
|
+
store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4105
4342
|
}
|
|
4106
4343
|
},
|
|
4107
4344
|
{
|
|
@@ -4153,62 +4390,75 @@ function createMeetingSession(options) {
|
|
|
4153
4390
|
if (entry !== null) {
|
|
4154
4391
|
store.upsertLobby(entry);
|
|
4155
4392
|
} else {
|
|
4156
|
-
store.setAdmission(attendee.identity, attendee.admissionState, null);
|
|
4393
|
+
store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4157
4394
|
}
|
|
4158
4395
|
}
|
|
4159
4396
|
}
|
|
4160
4397
|
});
|
|
4161
4398
|
meetingChannel.requestBackfill("manual");
|
|
4162
4399
|
},
|
|
4163
|
-
knock(roomName) {
|
|
4164
|
-
const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
|
|
4165
|
-
channel = handle;
|
|
4166
|
-
const entry = {
|
|
4167
|
-
identity: knockIdentity(),
|
|
4168
|
-
displayName: identity.displayName,
|
|
4169
|
-
avatarUrl: identity.avatarUrl,
|
|
4170
|
-
userId: identity.userId,
|
|
4171
|
-
state: "knocking",
|
|
4172
|
-
knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4173
|
-
decidedAt: null,
|
|
4174
|
-
decidedBy: null
|
|
4175
|
-
};
|
|
4176
|
-
store.upsertLobby(entry);
|
|
4177
|
-
handle.send(MEET_EVENTS.knock, entry);
|
|
4178
|
-
},
|
|
4179
4400
|
async admit(entry, admitted) {
|
|
4180
4401
|
const by = requireHostUser("meet.admit");
|
|
4181
4402
|
const meeting = requireMeeting("meet.admit");
|
|
4182
|
-
await repository.
|
|
4403
|
+
const row = await repository.decideAdmission({
|
|
4183
4404
|
meetingId: meeting.id,
|
|
4184
4405
|
identity: entry.identity,
|
|
4185
|
-
admitted,
|
|
4406
|
+
decision: admitted ? "admit" : "deny",
|
|
4186
4407
|
byUserId: by
|
|
4187
4408
|
});
|
|
4188
4409
|
store.upsertLobby({
|
|
4189
4410
|
...entry,
|
|
4190
|
-
state:
|
|
4411
|
+
state: decidedState(row.admissionState),
|
|
4191
4412
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4192
4413
|
decidedBy: by
|
|
4193
4414
|
});
|
|
4194
|
-
|
|
4415
|
+
hint();
|
|
4416
|
+
},
|
|
4417
|
+
async undeny(entry) {
|
|
4418
|
+
const by = requireHostUser("meet.undeny");
|
|
4419
|
+
const meeting = requireMeeting("meet.undeny");
|
|
4420
|
+
const row = await repository.decideAdmission({
|
|
4421
|
+
meetingId: meeting.id,
|
|
4195
4422
|
identity: entry.identity,
|
|
4196
|
-
|
|
4197
|
-
by
|
|
4423
|
+
decision: "undeny",
|
|
4424
|
+
byUserId: by
|
|
4198
4425
|
});
|
|
4426
|
+
store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
|
|
4427
|
+
hint();
|
|
4199
4428
|
},
|
|
4200
4429
|
async admitAll() {
|
|
4201
|
-
const
|
|
4202
|
-
|
|
4203
|
-
|
|
4430
|
+
const by = requireHostUser("meet.admitAll");
|
|
4431
|
+
const meeting = requireMeeting("meet.admitAll");
|
|
4432
|
+
const rows = await repository.admitAll(meeting.id, by);
|
|
4433
|
+
for (const row of rows) {
|
|
4434
|
+
store.setAdmission(row.identity, row.admissionState, by);
|
|
4204
4435
|
}
|
|
4436
|
+
hint();
|
|
4437
|
+
},
|
|
4438
|
+
startWaiting() {
|
|
4439
|
+
if (polling) return;
|
|
4440
|
+
polling = true;
|
|
4441
|
+
admittedOnce = false;
|
|
4442
|
+
listen();
|
|
4443
|
+
schedule(0);
|
|
4444
|
+
},
|
|
4445
|
+
stopWaiting,
|
|
4446
|
+
readState: readState2,
|
|
4447
|
+
async withdraw() {
|
|
4448
|
+
stopWaiting();
|
|
4449
|
+
await postLeave("withdraw");
|
|
4450
|
+
patchWaiting({ admission: "withdrawn" });
|
|
4451
|
+
hint();
|
|
4452
|
+
},
|
|
4453
|
+
async askAgain() {
|
|
4454
|
+
await options.rejoin?.();
|
|
4205
4455
|
},
|
|
4206
4456
|
async setLock(locked) {
|
|
4207
4457
|
const by = requireHostUser("meet.setLock");
|
|
4208
4458
|
const meeting = requireMeeting("meet.setLock");
|
|
4209
4459
|
const updated = await repository.setLock(meeting.id, locked, by);
|
|
4210
4460
|
store.patch({ meeting: updated, locked: updated.locked });
|
|
4211
|
-
|
|
4461
|
+
hint();
|
|
4212
4462
|
return updated;
|
|
4213
4463
|
},
|
|
4214
4464
|
/**
|
|
@@ -4266,6 +4516,7 @@ function createMeetingSession(options) {
|
|
|
4266
4516
|
return { endedAt, served: true };
|
|
4267
4517
|
},
|
|
4268
4518
|
dispose() {
|
|
4519
|
+
stopWaiting();
|
|
4269
4520
|
channel?.close();
|
|
4270
4521
|
channel = null;
|
|
4271
4522
|
meetingChannel?.close();
|
|
@@ -4385,7 +4636,6 @@ function kindOf(person) {
|
|
|
4385
4636
|
if (person.isAgent) return "assistant";
|
|
4386
4637
|
if (person.role === "host") return "host";
|
|
4387
4638
|
if (person.role === "cohost") return "cohost";
|
|
4388
|
-
if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
|
|
4389
4639
|
if (person.userId === null) return "guest";
|
|
4390
4640
|
return "member";
|
|
4391
4641
|
}
|
|
@@ -4670,7 +4920,7 @@ function meetingLength(meeting) {
|
|
|
4670
4920
|
if (start === null || end === null || end < start) return null;
|
|
4671
4921
|
return formatDurationMs(end - start, { style: "long" });
|
|
4672
4922
|
}
|
|
4673
|
-
async function loadMeetingChat(api, meetingId,
|
|
4923
|
+
async function loadMeetingChat(api, meetingId, meetingPass = null) {
|
|
4674
4924
|
const operation = "meet.loadMeetingChat";
|
|
4675
4925
|
let response;
|
|
4676
4926
|
try {
|
|
@@ -4678,8 +4928,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
|
|
|
4678
4928
|
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
4679
4929
|
method: "GET",
|
|
4680
4930
|
operation,
|
|
4681
|
-
requireAuth:
|
|
4682
|
-
...
|
|
4931
|
+
requireAuth: meetingPass === null,
|
|
4932
|
+
...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
|
|
4683
4933
|
});
|
|
4684
4934
|
} catch (error) {
|
|
4685
4935
|
return {
|
|
@@ -4823,7 +5073,12 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
|
|
|
4823
5073
|
// src/core/room.ts
|
|
4824
5074
|
var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
|
|
4825
5075
|
var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
|
|
4826
|
-
var
|
|
5076
|
+
var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
|
|
5077
|
+
var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
|
|
5078
|
+
function parseStanding(value) {
|
|
5079
|
+
return KNOWN_STANDINGS.includes(value) ? value : null;
|
|
5080
|
+
}
|
|
5081
|
+
var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
4827
5082
|
function parseRole(value) {
|
|
4828
5083
|
return KNOWN_ROLES.includes(value) ? value : "participant";
|
|
4829
5084
|
}
|
|
@@ -4857,6 +5112,9 @@ function projectParticipant(driver, joinedAt) {
|
|
|
4857
5112
|
// ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
|
|
4858
5113
|
// server's; the attribute is simply the one that always arrives.
|
|
4859
5114
|
role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
|
|
5115
|
+
standing: parseStanding(
|
|
5116
|
+
driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
|
|
5117
|
+
),
|
|
4860
5118
|
canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
|
|
4861
5119
|
isLocal: driver.isLocal,
|
|
4862
5120
|
isAgent: metadata.is_agent === true,
|
|
@@ -4929,7 +5187,7 @@ function createRoomEngine(options) {
|
|
|
4929
5187
|
const normalized = normalizeMeetError(error, operation);
|
|
4930
5188
|
store.patch({
|
|
4931
5189
|
phase: "failed",
|
|
4932
|
-
failure: { message: normalized.message, remedy: normalized.remedy }
|
|
5190
|
+
failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
|
|
4933
5191
|
});
|
|
4934
5192
|
report(
|
|
4935
5193
|
normalized.code === "session-unavailable" ? "warn" : "error",
|
|
@@ -5119,7 +5377,7 @@ function createRoomEngine(options) {
|
|
|
5119
5377
|
}
|
|
5120
5378
|
};
|
|
5121
5379
|
async function connect(request) {
|
|
5122
|
-
store.patch({ phase: "requesting-token", failure: null });
|
|
5380
|
+
store.patch({ phase: "requesting-token", failure: null, waiting: null });
|
|
5123
5381
|
const token = await tokens.get({
|
|
5124
5382
|
roomName: request.roomName,
|
|
5125
5383
|
identity,
|
|
@@ -5133,16 +5391,27 @@ function createRoomEngine(options) {
|
|
|
5133
5391
|
breakoutRoomId: token.breakoutRoomId ?? null,
|
|
5134
5392
|
grants: {
|
|
5135
5393
|
role: token.role,
|
|
5394
|
+
standing: token.standing,
|
|
5136
5395
|
canRecord: token.canRecord,
|
|
5137
5396
|
canPublish: token.canPublish,
|
|
5138
5397
|
canSubscribe: token.canSubscribe
|
|
5139
5398
|
}
|
|
5140
5399
|
});
|
|
5141
|
-
if (token.lobby) {
|
|
5142
|
-
store.patch({
|
|
5400
|
+
if (token.lobby || !token.canSubscribe) {
|
|
5401
|
+
store.patch({
|
|
5402
|
+
phase: "lobby",
|
|
5403
|
+
waiting: {
|
|
5404
|
+
admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
|
|
5405
|
+
admittersPresent: null,
|
|
5406
|
+
reknockAllowed: true,
|
|
5407
|
+
knockExpiresAt: null,
|
|
5408
|
+
pollSeconds: token.pollSeconds ?? 3,
|
|
5409
|
+
liveVersion: 0
|
|
5410
|
+
}
|
|
5411
|
+
});
|
|
5143
5412
|
return;
|
|
5144
5413
|
}
|
|
5145
|
-
store.patch({ phase: "connecting" });
|
|
5414
|
+
store.patch({ phase: "connecting", waiting: null });
|
|
5146
5415
|
const created = await options.driverFactory(handlers);
|
|
5147
5416
|
if (disposed) {
|
|
5148
5417
|
await created.disconnect();
|
|
@@ -5478,7 +5747,7 @@ function createInRoomClient(options) {
|
|
|
5478
5747
|
}
|
|
5479
5748
|
function headers() {
|
|
5480
5749
|
const token = options.resolveRoomToken();
|
|
5481
|
-
return token === null ? void 0 : { [
|
|
5750
|
+
return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
|
|
5482
5751
|
}
|
|
5483
5752
|
function refusal2(response, operation, remedy) {
|
|
5484
5753
|
return refusalFromResponse(response, operation, remedy);
|
|
@@ -5959,7 +6228,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
|
|
|
5959
6228
|
async function loadGuestMeetingRecord(args) {
|
|
5960
6229
|
const { api, repository, meetingId, meeting, roomToken } = args;
|
|
5961
6230
|
const session = args.session === true;
|
|
5962
|
-
if (roomToken
|
|
6231
|
+
if (roomToken?.meetingPass == null && !session) {
|
|
5963
6232
|
return refusedBundle(
|
|
5964
6233
|
meeting,
|
|
5965
6234
|
"This meeting's record is only readable by the people who were in it.",
|
|
@@ -5969,12 +6238,12 @@ async function loadGuestMeetingRecord(args) {
|
|
|
5969
6238
|
let response;
|
|
5970
6239
|
try {
|
|
5971
6240
|
response = await api.request({
|
|
5972
|
-
path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}
|
|
6241
|
+
path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
|
|
5973
6242
|
method: "GET",
|
|
5974
6243
|
// A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
|
|
5975
6244
|
requireAuth: session,
|
|
5976
6245
|
operation: "meet.record",
|
|
5977
|
-
headers: roomToken
|
|
6246
|
+
headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
|
|
5978
6247
|
});
|
|
5979
6248
|
} catch (error) {
|
|
5980
6249
|
return refusedBundle(
|
|
@@ -6362,6 +6631,10 @@ function takeMoveRequest(store) {
|
|
|
6362
6631
|
}
|
|
6363
6632
|
|
|
6364
6633
|
// src/core/observation.ts
|
|
6634
|
+
function observedRole(role, standing) {
|
|
6635
|
+
if (role === "host" || role === "cohost") return role;
|
|
6636
|
+
return isLinkFollower(standing) ? "guest" : "participant";
|
|
6637
|
+
}
|
|
6365
6638
|
function observedDevice(track) {
|
|
6366
6639
|
if (track.enabled) return "on";
|
|
6367
6640
|
const refused = track.unavailable;
|
|
@@ -6389,13 +6662,15 @@ function observedPhase(input) {
|
|
|
6389
6662
|
const raw = snapshot.phase;
|
|
6390
6663
|
const live = raw === "connected" || raw === "reconnecting";
|
|
6391
6664
|
if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
|
|
6665
|
+
const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
|
|
6666
|
+
if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
|
|
6392
6667
|
switch (raw) {
|
|
6393
6668
|
case "idle":
|
|
6394
6669
|
case "failed":
|
|
6395
6670
|
case "left":
|
|
6396
6671
|
return "prejoin";
|
|
6397
6672
|
case "lobby":
|
|
6398
|
-
return
|
|
6673
|
+
return waitingPhase(snapshot);
|
|
6399
6674
|
case "requesting-token":
|
|
6400
6675
|
case "connecting":
|
|
6401
6676
|
return "joining";
|
|
@@ -6404,20 +6679,39 @@ function observedPhase(input) {
|
|
|
6404
6679
|
return "in_call";
|
|
6405
6680
|
}
|
|
6406
6681
|
}
|
|
6682
|
+
function waitingPhase(snapshot) {
|
|
6683
|
+
switch (snapshot.waiting?.admission) {
|
|
6684
|
+
case "waiting_host":
|
|
6685
|
+
return "waiting:host_not_started";
|
|
6686
|
+
case "denied":
|
|
6687
|
+
return "denied";
|
|
6688
|
+
case "expired":
|
|
6689
|
+
return "knock_expired";
|
|
6690
|
+
case "withdrawn":
|
|
6691
|
+
return "left";
|
|
6692
|
+
case "ended":
|
|
6693
|
+
return "ended";
|
|
6694
|
+
case "removed":
|
|
6695
|
+
return "refused:removed";
|
|
6696
|
+
default:
|
|
6697
|
+
return "waiting:knocking";
|
|
6698
|
+
}
|
|
6699
|
+
}
|
|
6407
6700
|
var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
|
|
6408
6701
|
function observe(input) {
|
|
6409
6702
|
const { snapshot } = input;
|
|
6410
6703
|
const phase = observedPhase(input);
|
|
6411
6704
|
const inCall = phase === "in_call";
|
|
6412
|
-
const
|
|
6413
|
-
const canAdmit =
|
|
6705
|
+
const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
|
|
6706
|
+
const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
|
|
6707
|
+
const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
|
|
6414
6708
|
const deviceInPlay = phase === "joining" || inCall;
|
|
6415
6709
|
return {
|
|
6416
6710
|
phase,
|
|
6417
|
-
reason: null,
|
|
6711
|
+
reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
|
|
6418
6712
|
role,
|
|
6419
6713
|
connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
|
|
6420
|
-
hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
|
|
6714
|
+
hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
|
|
6421
6715
|
lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
|
|
6422
6716
|
recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
|
|
6423
6717
|
audioBlocked: null,
|
|
@@ -6480,7 +6774,7 @@ function participantAttributes(p) {
|
|
|
6480
6774
|
"data-meet-participant": "",
|
|
6481
6775
|
"data-meet-participant-identity": p.identity,
|
|
6482
6776
|
"data-meet-participant-name": p.displayName,
|
|
6483
|
-
"data-meet-participant-role": p.role,
|
|
6777
|
+
"data-meet-participant-role": observedRole(p.role, p.standing),
|
|
6484
6778
|
"data-meet-participant-self": String(p.isLocal)
|
|
6485
6779
|
};
|
|
6486
6780
|
const quality = observedQuality(p.quality);
|
|
@@ -6513,6 +6807,7 @@ export {
|
|
|
6513
6807
|
LISTEN_ONLY_NOTICE,
|
|
6514
6808
|
MEETING_INVITE_KIND,
|
|
6515
6809
|
MEETING_INVITE_VERSION,
|
|
6810
|
+
MEETING_PASS_HEADER,
|
|
6516
6811
|
MEETING_RECORD_GRACE_MS,
|
|
6517
6812
|
MEET_ANSWER_CONTEXT_EVENT,
|
|
6518
6813
|
MEET_ANSWER_EVENTS,
|
|
@@ -6528,12 +6823,12 @@ export {
|
|
|
6528
6823
|
ORGANIZATION_HEADER,
|
|
6529
6824
|
PARTICIPANT_CAN_RECORD_ATTRIBUTE,
|
|
6530
6825
|
PARTICIPANT_ROLE_ATTRIBUTE,
|
|
6826
|
+
PARTICIPANT_STANDING_ATTRIBUTE,
|
|
6531
6827
|
REACTION_TTL_MS,
|
|
6532
6828
|
REFUSED_ABSENCE,
|
|
6533
6829
|
ROLE_RANK,
|
|
6534
6830
|
ROOM_EVENTS,
|
|
6535
6831
|
ROOM_EVENT_PREFIX,
|
|
6536
|
-
ROOM_TOKEN_HEADER,
|
|
6537
6832
|
RPCS,
|
|
6538
6833
|
TABLES,
|
|
6539
6834
|
TRANSCRIPTION_ATTRIBUTES,
|
|
@@ -6604,6 +6899,7 @@ export {
|
|
|
6604
6899
|
isGuest,
|
|
6605
6900
|
isGuestIdentity,
|
|
6606
6901
|
isHostRole,
|
|
6902
|
+
isLinkFollower,
|
|
6607
6903
|
isListenOnly,
|
|
6608
6904
|
isNoteTaker,
|
|
6609
6905
|
isRealName,
|
|
@@ -6622,6 +6918,7 @@ export {
|
|
|
6622
6918
|
meetingInviteAction,
|
|
6623
6919
|
meetingLength,
|
|
6624
6920
|
meetingLink,
|
|
6921
|
+
meetingPassHeaders,
|
|
6625
6922
|
meetingPath,
|
|
6626
6923
|
meetingSlug,
|
|
6627
6924
|
meetingTopic,
|
|
@@ -6665,6 +6962,7 @@ export {
|
|
|
6665
6962
|
recordingLabel,
|
|
6666
6963
|
recordingNotice,
|
|
6667
6964
|
recordingOffsetMs,
|
|
6965
|
+
rememberedMeetingPass,
|
|
6668
6966
|
roleOf,
|
|
6669
6967
|
roomTopic,
|
|
6670
6968
|
rootAttributes,
|