@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/core.cjs
CHANGED
|
@@ -50,6 +50,7 @@ __export(core_exports, {
|
|
|
50
50
|
LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
|
|
51
51
|
MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
|
|
52
52
|
MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
|
|
53
|
+
MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
|
|
53
54
|
MEETING_RECORD_GRACE_MS: () => MEETING_RECORD_GRACE_MS,
|
|
54
55
|
MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
|
|
55
56
|
MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
|
|
@@ -65,12 +66,12 @@ __export(core_exports, {
|
|
|
65
66
|
ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
|
|
66
67
|
PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
|
|
67
68
|
PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
|
|
69
|
+
PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
|
|
68
70
|
REACTION_TTL_MS: () => REACTION_TTL_MS,
|
|
69
71
|
REFUSED_ABSENCE: () => REFUSED_ABSENCE,
|
|
70
72
|
ROLE_RANK: () => ROLE_RANK,
|
|
71
73
|
ROOM_EVENTS: () => ROOM_EVENTS,
|
|
72
74
|
ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
|
|
73
|
-
ROOM_TOKEN_HEADER: () => ROOM_TOKEN_HEADER,
|
|
74
75
|
RPCS: () => RPCS,
|
|
75
76
|
TABLES: () => TABLES,
|
|
76
77
|
TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
|
|
@@ -141,6 +142,7 @@ __export(core_exports, {
|
|
|
141
142
|
isGuest: () => isGuest,
|
|
142
143
|
isGuestIdentity: () => isGuestIdentity,
|
|
143
144
|
isHostRole: () => isHostRole,
|
|
145
|
+
isLinkFollower: () => isLinkFollower,
|
|
144
146
|
isListenOnly: () => isListenOnly,
|
|
145
147
|
isNoteTaker: () => isNoteTaker,
|
|
146
148
|
isRealName: () => isRealName,
|
|
@@ -159,6 +161,7 @@ __export(core_exports, {
|
|
|
159
161
|
meetingInviteAction: () => meetingInviteAction,
|
|
160
162
|
meetingLength: () => meetingLength,
|
|
161
163
|
meetingLink: () => meetingLink,
|
|
164
|
+
meetingPassHeaders: () => meetingPassHeaders,
|
|
162
165
|
meetingPath: () => meetingPath,
|
|
163
166
|
meetingSlug: () => meetingSlug,
|
|
164
167
|
meetingTopic: () => meetingTopic,
|
|
@@ -202,6 +205,7 @@ __export(core_exports, {
|
|
|
202
205
|
recordingLabel: () => recordingLabel,
|
|
203
206
|
recordingNotice: () => recordingNotice,
|
|
204
207
|
recordingOffsetMs: () => recordingOffsetMs,
|
|
208
|
+
rememberedMeetingPass: () => rememberedMeetingPass,
|
|
205
209
|
roleOf: () => roleOf,
|
|
206
210
|
roomTopic: () => roomTopic,
|
|
207
211
|
rootAttributes: () => rootAttributes,
|
|
@@ -236,11 +240,18 @@ var MeetError = class extends Error {
|
|
|
236
240
|
/** What to DO. Rendered to the user for the device codes. */
|
|
237
241
|
remedy;
|
|
238
242
|
cause;
|
|
239
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
|
|
245
|
+
* `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
|
|
246
|
+
* renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
|
|
247
|
+
*/
|
|
248
|
+
reason;
|
|
249
|
+
constructor(code, message, remedy, cause, reason) {
|
|
240
250
|
super(message);
|
|
241
251
|
this.name = "MeetError";
|
|
242
252
|
this.code = code;
|
|
243
253
|
this.remedy = remedy;
|
|
254
|
+
this.reason = reason ?? null;
|
|
244
255
|
if (cause !== void 0) this.cause = cause;
|
|
245
256
|
}
|
|
246
257
|
/** True when retrying after the host re-establishes a session is the fix. */
|
|
@@ -458,6 +469,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
|
|
|
458
469
|
}
|
|
459
470
|
};
|
|
460
471
|
}
|
|
472
|
+
function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
|
|
473
|
+
const raw = storage.read(roomName);
|
|
474
|
+
if (raw === null) return null;
|
|
475
|
+
try {
|
|
476
|
+
const parsed = JSON.parse(raw);
|
|
477
|
+
if (parsed.standing !== "guest") return null;
|
|
478
|
+
return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
|
|
479
|
+
} catch {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
461
483
|
var MEET_ROUTES = {
|
|
462
484
|
token: "/api/v1/meet/token",
|
|
463
485
|
recordingStart: "/api/v1/meet/recording/start",
|
|
@@ -471,7 +493,7 @@ var MEET_ROUTES = {
|
|
|
471
493
|
/**
|
|
472
494
|
* THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
|
|
473
495
|
* authorized by the room token this device already holds, carried in the
|
|
474
|
-
* `x-meet-
|
|
496
|
+
* `x-meet-pass` header — never a query parameter, because a bearer
|
|
475
497
|
* credential in a URL lands in access logs, referrers and history.
|
|
476
498
|
*/
|
|
477
499
|
record: "/api/v1/meet/record",
|
|
@@ -479,7 +501,7 @@ var MEET_ROUTES = {
|
|
|
479
501
|
recordAccess: "/api/v1/meet/record/access",
|
|
480
502
|
/**
|
|
481
503
|
* KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
|
|
482
|
-
* their kept pass in `x-meet-
|
|
504
|
+
* their kept pass in `x-meet-pass` with their new session, and the
|
|
483
505
|
* meeting they attended becomes theirs to read. Idempotent; never a grant.
|
|
484
506
|
*/
|
|
485
507
|
claim: "/api/v1/meet/claim",
|
|
@@ -501,7 +523,7 @@ var MEET_ROUTES = {
|
|
|
501
523
|
* Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
|
|
502
524
|
* unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
|
|
503
525
|
* Decided by the server from the meeting itself; a guest's pass rides
|
|
504
|
-
* `x-meet-
|
|
526
|
+
* `x-meet-pass`.
|
|
505
527
|
*/
|
|
506
528
|
moderate: "/api/v1/meet/moderate",
|
|
507
529
|
/** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
|
|
@@ -512,12 +534,26 @@ var MEET_ROUTES = {
|
|
|
512
534
|
*/
|
|
513
535
|
collab: "/api/v1/meet/collab",
|
|
514
536
|
/** Meet wave 5: translate finished caption lines into this viewer's language. */
|
|
515
|
-
captionsTranslate: "/api/v1/meet/captions/translate"
|
|
537
|
+
captionsTranslate: "/api/v1/meet/captions/translate",
|
|
538
|
+
/**
|
|
539
|
+
* S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
|
|
540
|
+
* person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
|
|
541
|
+
* phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
|
|
542
|
+
*/
|
|
543
|
+
state: "/api/v1/meet/state",
|
|
544
|
+
/** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
|
|
545
|
+
leave: "/api/v1/meet/leave",
|
|
546
|
+
/** S4: a pass holder (guests included) saw the recording notice, for this run. */
|
|
547
|
+
consent: "/api/v1/meet/consent"
|
|
516
548
|
};
|
|
517
549
|
function meetingPath(template, meetingId) {
|
|
518
550
|
return template.replace("{meeting_id}", encodeURIComponent(meetingId));
|
|
519
551
|
}
|
|
520
|
-
var
|
|
552
|
+
var MEETING_PASS_HEADER = "x-meet-pass";
|
|
553
|
+
function meetingPassHeaders(token) {
|
|
554
|
+
const pass = token?.meetingPass ?? null;
|
|
555
|
+
return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
|
|
556
|
+
}
|
|
521
557
|
var REFRESH_RATIO = 0.8;
|
|
522
558
|
function readString(source, key) {
|
|
523
559
|
const value = source[key];
|
|
@@ -527,7 +563,12 @@ function readBoolean(source, key, fallback) {
|
|
|
527
563
|
const value = source[key];
|
|
528
564
|
return typeof value === "boolean" ? value : fallback;
|
|
529
565
|
}
|
|
530
|
-
var
|
|
566
|
+
var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
|
|
567
|
+
function readStanding(source, key) {
|
|
568
|
+
const value = source[key];
|
|
569
|
+
return TOKEN_STANDINGS.includes(value) ? value : null;
|
|
570
|
+
}
|
|
571
|
+
var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
531
572
|
function readRole(source, key) {
|
|
532
573
|
const value = source[key];
|
|
533
574
|
return TOKEN_ROLES.includes(value) ? value : null;
|
|
@@ -570,8 +611,15 @@ function parseRoomToken(body, roomName) {
|
|
|
570
611
|
// Deriving the role from what DID arrive keeps a host a host against a
|
|
571
612
|
// server that has not landed MRI-A11 — and never invents a host from
|
|
572
613
|
// silence, because both fallbacks below grant nothing.
|
|
573
|
-
|
|
614
|
+
// A word this package does not know grants nothing: `participant`, never a host.
|
|
615
|
+
role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
|
|
616
|
+
standing: readStanding(payload, "standing"),
|
|
617
|
+
meetingPass: readString(payload, "meeting_pass"),
|
|
618
|
+
meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
|
|
619
|
+
sessionId: readString(payload, "session_id"),
|
|
574
620
|
lobby: readBoolean(payload, "lobby", false),
|
|
621
|
+
admissionState: readString(payload, "admission_state") ?? void 0,
|
|
622
|
+
pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
|
|
575
623
|
breakoutRoomId: readString(payload, "breakout_room_id") ?? null
|
|
576
624
|
};
|
|
577
625
|
}
|
|
@@ -581,7 +629,7 @@ function createTokenClient(options) {
|
|
|
581
629
|
const storage = options.storage;
|
|
582
630
|
function remember(token) {
|
|
583
631
|
if (storage === void 0) return;
|
|
584
|
-
if (
|
|
632
|
+
if (token.standing !== "guest") return;
|
|
585
633
|
storage.write(token.roomName, JSON.stringify(token));
|
|
586
634
|
}
|
|
587
635
|
function recall(roomName) {
|
|
@@ -639,22 +687,28 @@ function createTokenClient(options) {
|
|
|
639
687
|
throw new MeetError(
|
|
640
688
|
"forbidden",
|
|
641
689
|
said?.message ?? "You can't join this meeting.",
|
|
642
|
-
said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
|
|
690
|
+
said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
|
|
691
|
+
void 0,
|
|
692
|
+
said?.reason ?? null
|
|
643
693
|
);
|
|
644
694
|
}
|
|
645
695
|
if (response.status === 404) {
|
|
646
696
|
throw new MeetError(
|
|
647
697
|
"not-found",
|
|
648
698
|
"meet.token: no such meeting",
|
|
649
|
-
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
|
|
699
|
+
"The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
|
|
700
|
+
void 0,
|
|
701
|
+
"not_found"
|
|
650
702
|
);
|
|
651
703
|
}
|
|
652
704
|
if (response.status === 410) {
|
|
653
|
-
const
|
|
705
|
+
const said = await serverSentence(response);
|
|
654
706
|
throw new MeetError(
|
|
655
707
|
"cancelled",
|
|
656
|
-
|
|
657
|
-
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
|
|
708
|
+
said?.message ?? "This meeting was cancelled.",
|
|
709
|
+
"Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
|
|
710
|
+
void 0,
|
|
711
|
+
said?.reason ?? "cancelled"
|
|
658
712
|
);
|
|
659
713
|
}
|
|
660
714
|
if (response.status === 423) {
|
|
@@ -662,7 +716,9 @@ function createTokenClient(options) {
|
|
|
662
716
|
throw new MeetError(
|
|
663
717
|
"room-locked",
|
|
664
718
|
said?.message ?? "The meeting is locked.",
|
|
665
|
-
said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
|
|
719
|
+
said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
|
|
720
|
+
void 0,
|
|
721
|
+
said?.reason ?? "locked"
|
|
666
722
|
);
|
|
667
723
|
}
|
|
668
724
|
if (response.status === 503) {
|
|
@@ -731,6 +787,10 @@ function placeholder(roomName) {
|
|
|
731
787
|
canRecord: false,
|
|
732
788
|
isHost: false,
|
|
733
789
|
role: "participant",
|
|
790
|
+
standing: null,
|
|
791
|
+
meetingPass: null,
|
|
792
|
+
meetingPassExpiresAt: null,
|
|
793
|
+
sessionId: null,
|
|
734
794
|
lobby: false,
|
|
735
795
|
breakoutRoomId: null
|
|
736
796
|
};
|
|
@@ -743,7 +803,8 @@ async function serverSentence(response) {
|
|
|
743
803
|
const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
|
|
744
804
|
if (message === null) return null;
|
|
745
805
|
const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
|
|
746
|
-
|
|
806
|
+
const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
|
|
807
|
+
return { message, remedy, reason };
|
|
747
808
|
} catch {
|
|
748
809
|
return null;
|
|
749
810
|
}
|
|
@@ -932,7 +993,7 @@ function createMeetAi(options) {
|
|
|
932
993
|
operation: "meet.ai.ask",
|
|
933
994
|
// A GUEST MAY ASK. The server decides whether it can answer.
|
|
934
995
|
requireAuth: false,
|
|
935
|
-
...roomToken !== null ? { headers: { [
|
|
996
|
+
...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
|
|
936
997
|
...signal !== void 0 ? { signal } : {}
|
|
937
998
|
});
|
|
938
999
|
if (!response.ok) {
|
|
@@ -1171,14 +1232,15 @@ var MEET_EVENTS = {
|
|
|
1171
1232
|
cancel: "mx.call.cancel",
|
|
1172
1233
|
/** You answered (or declined) on another device. Stop ringing here. */
|
|
1173
1234
|
settled: "mx.call.settled",
|
|
1174
|
-
/**
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1235
|
+
/**
|
|
1236
|
+
* 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
|
|
1237
|
+
* read again". Nothing a person must see rides it: a waiting person learns admitted /
|
|
1238
|
+
* denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
|
|
1239
|
+
* every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
|
|
1240
|
+
* `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
|
|
1241
|
+
* server publishes the same event on the same topic (aidream `admission.room_topic`).
|
|
1242
|
+
*/
|
|
1243
|
+
stateChanged: "mx.state.changed"
|
|
1182
1244
|
};
|
|
1183
1245
|
|
|
1184
1246
|
// src/core/types.ts
|
|
@@ -1192,14 +1254,19 @@ function isGuest(identity) {
|
|
|
1192
1254
|
return identity.userId === null;
|
|
1193
1255
|
}
|
|
1194
1256
|
var ROLE_RANK = {
|
|
1195
|
-
host:
|
|
1196
|
-
cohost:
|
|
1257
|
+
host: 4,
|
|
1258
|
+
cohost: 3,
|
|
1259
|
+
presenter: 2,
|
|
1197
1260
|
participant: 1,
|
|
1198
|
-
|
|
1261
|
+
viewer: 0
|
|
1199
1262
|
};
|
|
1263
|
+
function isLinkFollower(standing) {
|
|
1264
|
+
return standing === "guest" || standing === "outsider";
|
|
1265
|
+
}
|
|
1200
1266
|
function isHostRole(role) {
|
|
1201
1267
|
return role === "host" || role === "cohost";
|
|
1202
1268
|
}
|
|
1269
|
+
var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
|
|
1203
1270
|
|
|
1204
1271
|
// src/core/calls.ts
|
|
1205
1272
|
var TRANSITIONS = {
|
|
@@ -1749,15 +1816,15 @@ var CAPTION_LANGUAGES = [
|
|
|
1749
1816
|
{ tag: "id", name: "Indonesian" },
|
|
1750
1817
|
{ tag: "sv", name: "Swedish" }
|
|
1751
1818
|
];
|
|
1752
|
-
async function loadMeetingActivities(api, meetingId,
|
|
1819
|
+
async function loadMeetingActivities(api, meetingId, meetingPass = null) {
|
|
1753
1820
|
let response;
|
|
1754
1821
|
try {
|
|
1755
1822
|
response = await api.request({
|
|
1756
1823
|
path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
1757
1824
|
method: "GET",
|
|
1758
1825
|
operation: "meet.loadMeetingActivities",
|
|
1759
|
-
requireAuth:
|
|
1760
|
-
...
|
|
1826
|
+
requireAuth: meetingPass === null,
|
|
1827
|
+
...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
|
|
1761
1828
|
});
|
|
1762
1829
|
} catch (error) {
|
|
1763
1830
|
return {
|
|
@@ -1812,9 +1879,15 @@ var RPCS = {
|
|
|
1812
1879
|
getOrCreateMeeting: "meet_get_or_create_meeting",
|
|
1813
1880
|
/** Resolve a durable public link, before/during/after its scheduled time. */
|
|
1814
1881
|
meetingBySlug: "meet_meeting_by_slug",
|
|
1815
|
-
|
|
1882
|
+
/*
|
|
1883
|
+
* ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
|
|
1884
|
+
* sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
|
|
1885
|
+
*/
|
|
1886
|
+
admit: "meet_admit",
|
|
1887
|
+
deny: "meet_deny",
|
|
1888
|
+
undeny: "meet_undeny",
|
|
1889
|
+
admitAll: "meet_admit_all",
|
|
1816
1890
|
setLock: "meet_set_lock",
|
|
1817
|
-
endMeeting: "meet_end_meeting",
|
|
1818
1891
|
recordConsent: "meet_record_consent",
|
|
1819
1892
|
/*
|
|
1820
1893
|
* ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
|
|
@@ -1856,13 +1929,13 @@ var RPCS = {
|
|
|
1856
1929
|
/** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
|
|
1857
1930
|
search: "meet_search"
|
|
1858
1931
|
};
|
|
1859
|
-
var VOID_RPCS = [RPCS.
|
|
1932
|
+
var VOID_RPCS = [RPCS.recordConsent];
|
|
1860
1933
|
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";
|
|
1861
1934
|
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";
|
|
1862
1935
|
var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
|
|
1863
1936
|
var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
|
|
1864
1937
|
var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
|
|
1865
|
-
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";
|
|
1938
|
+
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";
|
|
1866
1939
|
function projectStays(value) {
|
|
1867
1940
|
if (!Array.isArray(value)) return [];
|
|
1868
1941
|
const stays = [];
|
|
@@ -2228,8 +2301,17 @@ function createMeetRepository(options) {
|
|
|
2228
2301
|
const operation = "meet.projectAttendee";
|
|
2229
2302
|
const role = text(row, "role");
|
|
2230
2303
|
const admission = text(row, "admission_state");
|
|
2231
|
-
const known = ["host", "cohost", "participant", "
|
|
2232
|
-
const states = [
|
|
2304
|
+
const known = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
2305
|
+
const states = [
|
|
2306
|
+
"waiting_host",
|
|
2307
|
+
"knocking",
|
|
2308
|
+
"admitted",
|
|
2309
|
+
"denied",
|
|
2310
|
+
"expired",
|
|
2311
|
+
"withdrawn",
|
|
2312
|
+
"removed",
|
|
2313
|
+
"ended"
|
|
2314
|
+
];
|
|
2233
2315
|
const userId = text(row, "participant_user_id");
|
|
2234
2316
|
const identity = requireText(row, "identity", operation);
|
|
2235
2317
|
return {
|
|
@@ -2243,6 +2325,8 @@ function createMeetRepository(options) {
|
|
|
2243
2325
|
userId: userId === null ? null : asUserId(userId),
|
|
2244
2326
|
role: known.includes(role) ? role : "participant",
|
|
2245
2327
|
admissionState: states.includes(admission) ? admission : "admitted",
|
|
2328
|
+
knockedAt: text(row, "knocked_at"),
|
|
2329
|
+
knockExpiresAt: text(row, "knock_expires_at"),
|
|
2246
2330
|
isAgent: bool(row, "is_agent", false),
|
|
2247
2331
|
joinedAt: text(row, "joined_at"),
|
|
2248
2332
|
leftAt: text(row, "left_at"),
|
|
@@ -2521,16 +2605,26 @@ function createMeetRepository(options) {
|
|
|
2521
2605
|
hasMore: rows.length > limit
|
|
2522
2606
|
};
|
|
2523
2607
|
},
|
|
2524
|
-
async
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2608
|
+
async decideAdmission({ meetingId, identity, decision, byUserId }) {
|
|
2609
|
+
const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
|
|
2610
|
+
const row = await guard(
|
|
2611
|
+
`meet.${decision}`,
|
|
2612
|
+
() => db.rpc(fn, {
|
|
2528
2613
|
p_meeting_id: meetingId,
|
|
2529
2614
|
p_identity: identity,
|
|
2530
|
-
p_admitted: admitted,
|
|
2531
2615
|
p_by_user_id: byUserId
|
|
2532
2616
|
})
|
|
2533
2617
|
);
|
|
2618
|
+
return projectAttendee(row);
|
|
2619
|
+
},
|
|
2620
|
+
async admitAll(meetingId, byUserId) {
|
|
2621
|
+
const response = await db.rpc(RPCS.admitAll, {
|
|
2622
|
+
p_meeting_id: meetingId,
|
|
2623
|
+
p_by_user_id: byUserId
|
|
2624
|
+
});
|
|
2625
|
+
if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
|
|
2626
|
+
const rows = Array.isArray(response.data) ? response.data : [];
|
|
2627
|
+
return rows.map(projectAttendee);
|
|
2534
2628
|
},
|
|
2535
2629
|
async setLock(meetingId, locked, byUserId) {
|
|
2536
2630
|
const row = await guard(
|
|
@@ -2543,16 +2637,6 @@ function createMeetRepository(options) {
|
|
|
2543
2637
|
);
|
|
2544
2638
|
return projectMeeting(row);
|
|
2545
2639
|
},
|
|
2546
|
-
async endMeeting(meetingId, byUserId) {
|
|
2547
|
-
const row = await guard(
|
|
2548
|
-
"meet.endMeeting",
|
|
2549
|
-
() => db.rpc(RPCS.endMeeting, {
|
|
2550
|
-
p_meeting_id: meetingId,
|
|
2551
|
-
p_by_user_id: byUserId
|
|
2552
|
-
})
|
|
2553
|
-
);
|
|
2554
|
-
return projectMeeting(row);
|
|
2555
|
-
},
|
|
2556
2640
|
async transcript(meetingId, limit = 1e3) {
|
|
2557
2641
|
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);
|
|
2558
2642
|
if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
|
|
@@ -2881,12 +2965,14 @@ var DEFAULT_EFFECTS = {
|
|
|
2881
2965
|
};
|
|
2882
2966
|
var NO_GRANTS = {
|
|
2883
2967
|
role: "participant",
|
|
2968
|
+
standing: null,
|
|
2884
2969
|
canRecord: false,
|
|
2885
2970
|
canPublish: false,
|
|
2886
2971
|
canSubscribe: false
|
|
2887
2972
|
};
|
|
2888
2973
|
var EMPTY = {
|
|
2889
2974
|
phase: "idle",
|
|
2975
|
+
waiting: null,
|
|
2890
2976
|
meeting: null,
|
|
2891
2977
|
localIdentity: null,
|
|
2892
2978
|
grants: NO_GRANTS,
|
|
@@ -2927,6 +3013,11 @@ function effectiveRole(snapshot) {
|
|
|
2927
3013
|
const fromToken = snapshot.grants.role;
|
|
2928
3014
|
return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
|
|
2929
3015
|
}
|
|
3016
|
+
function effectiveStanding(snapshot) {
|
|
3017
|
+
if (snapshot.grants.standing != null) return snapshot.grants.standing;
|
|
3018
|
+
const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
|
|
3019
|
+
return me?.standing ?? null;
|
|
3020
|
+
}
|
|
2930
3021
|
var REACTION_TTL_MS = 5e3;
|
|
2931
3022
|
var CHAT_LIMIT = 200;
|
|
2932
3023
|
function createMeetStore(initial = {}) {
|
|
@@ -3419,7 +3510,7 @@ function applyModeration(store, payload, now) {
|
|
|
3419
3510
|
return ignored(`a moderation kind this version does not know: ${String(kind)}`);
|
|
3420
3511
|
}
|
|
3421
3512
|
}
|
|
3422
|
-
var ROLES = ["host", "cohost", "participant", "
|
|
3513
|
+
var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
3423
3514
|
|
|
3424
3515
|
// src/core/devices.ts
|
|
3425
3516
|
var KINDS = ["audioinput", "videoinput", "audiooutput"];
|
|
@@ -4159,19 +4250,25 @@ function roleOf(participants, identity) {
|
|
|
4159
4250
|
return participants.find((p) => p.identity === identity)?.role ?? "participant";
|
|
4160
4251
|
}
|
|
4161
4252
|
var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
|
|
4162
|
-
function lobbyEntryFrom(attendee) {
|
|
4163
|
-
if (attendee.admissionState
|
|
4253
|
+
function lobbyEntryFrom(attendee, now = Date.now()) {
|
|
4254
|
+
if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
|
|
4255
|
+
const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
|
|
4256
|
+
if (!Number.isNaN(expires) && expires <= now) return null;
|
|
4164
4257
|
return {
|
|
4165
4258
|
identity: attendee.identity,
|
|
4166
4259
|
displayName: attendee.displayName,
|
|
4167
4260
|
avatarUrl: attendee.avatarUrl,
|
|
4168
4261
|
userId: attendee.userId,
|
|
4169
4262
|
state: "knocking",
|
|
4170
|
-
knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4263
|
+
knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4264
|
+
knockExpiresAt: attendee.knockExpiresAt ?? null,
|
|
4171
4265
|
decidedAt: null,
|
|
4172
4266
|
decidedBy: null
|
|
4173
4267
|
};
|
|
4174
4268
|
}
|
|
4269
|
+
function decidedState(state) {
|
|
4270
|
+
return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
|
|
4271
|
+
}
|
|
4175
4272
|
function createMeetingSession(options) {
|
|
4176
4273
|
const { manager, repository, store, identity } = options;
|
|
4177
4274
|
let channel = null;
|
|
@@ -4195,11 +4292,6 @@ function createMeetingSession(options) {
|
|
|
4195
4292
|
}
|
|
4196
4293
|
return identity.userId;
|
|
4197
4294
|
}
|
|
4198
|
-
function knockIdentity() {
|
|
4199
|
-
return asParticipantIdentity(
|
|
4200
|
-
identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
|
|
4201
|
-
);
|
|
4202
|
-
}
|
|
4203
4295
|
function requireMeeting(operation) {
|
|
4204
4296
|
const meeting = store.snapshot().meeting;
|
|
4205
4297
|
if (meeting === null) {
|
|
@@ -4211,6 +4303,191 @@ function createMeetingSession(options) {
|
|
|
4211
4303
|
}
|
|
4212
4304
|
return meeting;
|
|
4213
4305
|
}
|
|
4306
|
+
let polling = false;
|
|
4307
|
+
let pollTimer = null;
|
|
4308
|
+
let reading = null;
|
|
4309
|
+
let admittedOnce = false;
|
|
4310
|
+
let unlisten = null;
|
|
4311
|
+
function report(level, message, remedy) {
|
|
4312
|
+
options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
|
|
4313
|
+
}
|
|
4314
|
+
function pollMs() {
|
|
4315
|
+
const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
|
|
4316
|
+
return Math.max(1, Math.min(60, seconds)) * 1e3;
|
|
4317
|
+
}
|
|
4318
|
+
function schedule(ms2) {
|
|
4319
|
+
if (pollTimer !== null) clearTimeout(pollTimer);
|
|
4320
|
+
if (!polling) return;
|
|
4321
|
+
pollTimer = setTimeout(() => {
|
|
4322
|
+
pollTimer = null;
|
|
4323
|
+
void readState2().finally(() => schedule(pollMs()));
|
|
4324
|
+
}, ms2);
|
|
4325
|
+
}
|
|
4326
|
+
function listen() {
|
|
4327
|
+
if (unlisten !== null || typeof window === "undefined") return;
|
|
4328
|
+
const nudge = () => {
|
|
4329
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
4330
|
+
void readState2();
|
|
4331
|
+
};
|
|
4332
|
+
const goodbye = () => {
|
|
4333
|
+
const meeting = store.snapshot().meeting;
|
|
4334
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4335
|
+
const waiting = store.snapshot().waiting;
|
|
4336
|
+
if (meeting === null || pass === null || waiting === null) return;
|
|
4337
|
+
if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
|
|
4338
|
+
try {
|
|
4339
|
+
navigator.sendBeacon?.(
|
|
4340
|
+
options.api.url(MEET_ROUTES.leave),
|
|
4341
|
+
new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
|
|
4342
|
+
type: "application/json"
|
|
4343
|
+
})
|
|
4344
|
+
);
|
|
4345
|
+
} catch {
|
|
4346
|
+
}
|
|
4347
|
+
};
|
|
4348
|
+
window.addEventListener("online", nudge);
|
|
4349
|
+
window.addEventListener("pagehide", goodbye);
|
|
4350
|
+
document.addEventListener("visibilitychange", nudge);
|
|
4351
|
+
unlisten = () => {
|
|
4352
|
+
window.removeEventListener("online", nudge);
|
|
4353
|
+
window.removeEventListener("pagehide", goodbye);
|
|
4354
|
+
document.removeEventListener("visibilitychange", nudge);
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
function stopWaiting() {
|
|
4358
|
+
polling = false;
|
|
4359
|
+
if (pollTimer !== null) clearTimeout(pollTimer);
|
|
4360
|
+
pollTimer = null;
|
|
4361
|
+
unlisten?.();
|
|
4362
|
+
unlisten = null;
|
|
4363
|
+
}
|
|
4364
|
+
function patchWaiting(next) {
|
|
4365
|
+
const current = store.snapshot().waiting;
|
|
4366
|
+
const base = current ?? {
|
|
4367
|
+
admission: "knocking",
|
|
4368
|
+
admittersPresent: null,
|
|
4369
|
+
reknockAllowed: true,
|
|
4370
|
+
knockExpiresAt: null,
|
|
4371
|
+
pollSeconds: 3,
|
|
4372
|
+
liveVersion: 0
|
|
4373
|
+
};
|
|
4374
|
+
store.patch({ waiting: { ...base, ...next } });
|
|
4375
|
+
}
|
|
4376
|
+
function apply(read) {
|
|
4377
|
+
const snapshot = store.snapshot();
|
|
4378
|
+
if (snapshot.phase !== "lobby") return;
|
|
4379
|
+
const admission = read.admission_state ?? null;
|
|
4380
|
+
const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
|
|
4381
|
+
if (admission === "admitted" && !ended) {
|
|
4382
|
+
if (admittedOnce) return;
|
|
4383
|
+
admittedOnce = true;
|
|
4384
|
+
stopWaiting();
|
|
4385
|
+
options.onAdmitted?.();
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
if (ended) {
|
|
4389
|
+
stopWaiting();
|
|
4390
|
+
patchWaiting({ admission: "ended" });
|
|
4391
|
+
const meeting = snapshot.meeting;
|
|
4392
|
+
if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
|
|
4393
|
+
store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
|
|
4394
|
+
}
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
if (admission === null) return;
|
|
4398
|
+
patchWaiting({
|
|
4399
|
+
admission,
|
|
4400
|
+
admittersPresent: read.admitters_present ?? null,
|
|
4401
|
+
reknockAllowed: read.reknock_allowed ?? true,
|
|
4402
|
+
knockExpiresAt: read.knock_expires_at ?? null,
|
|
4403
|
+
pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
|
|
4404
|
+
liveVersion: read.live_version ?? 0
|
|
4405
|
+
});
|
|
4406
|
+
if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
|
|
4407
|
+
stopWaiting();
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
async function readState2() {
|
|
4411
|
+
if (reading !== null) return reading;
|
|
4412
|
+
const meeting = store.snapshot().meeting;
|
|
4413
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4414
|
+
if (meeting === null || pass === null) return;
|
|
4415
|
+
reading = (async () => {
|
|
4416
|
+
try {
|
|
4417
|
+
const response = await options.api.request({
|
|
4418
|
+
path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
|
|
4419
|
+
method: "GET",
|
|
4420
|
+
requireAuth: false,
|
|
4421
|
+
operation: "meet.state",
|
|
4422
|
+
headers: { [MEETING_PASS_HEADER]: pass }
|
|
4423
|
+
});
|
|
4424
|
+
if (!response.ok) {
|
|
4425
|
+
let body = null;
|
|
4426
|
+
try {
|
|
4427
|
+
body = await response.json();
|
|
4428
|
+
} catch {
|
|
4429
|
+
body = null;
|
|
4430
|
+
}
|
|
4431
|
+
const refusal2 = readServerRefusal(body);
|
|
4432
|
+
report(
|
|
4433
|
+
"warn",
|
|
4434
|
+
refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
|
|
4435
|
+
refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
|
|
4436
|
+
);
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
apply(await response.json());
|
|
4440
|
+
} catch (error) {
|
|
4441
|
+
report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
|
|
4442
|
+
} finally {
|
|
4443
|
+
reading = null;
|
|
4444
|
+
}
|
|
4445
|
+
})();
|
|
4446
|
+
return reading;
|
|
4447
|
+
}
|
|
4448
|
+
async function postLeave(intent) {
|
|
4449
|
+
const meeting = requireMeeting(`meet.${intent}`);
|
|
4450
|
+
const pass = options.resolvePass?.() ?? null;
|
|
4451
|
+
const response = await options.api.request({
|
|
4452
|
+
path: MEET_ROUTES.leave,
|
|
4453
|
+
method: "POST",
|
|
4454
|
+
requireAuth: false,
|
|
4455
|
+
operation: `meet.${intent}`,
|
|
4456
|
+
body: { meeting_id: meeting.id, intent },
|
|
4457
|
+
...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
|
|
4458
|
+
});
|
|
4459
|
+
if (!response.ok) {
|
|
4460
|
+
let body = null;
|
|
4461
|
+
try {
|
|
4462
|
+
body = await response.json();
|
|
4463
|
+
} catch {
|
|
4464
|
+
body = null;
|
|
4465
|
+
}
|
|
4466
|
+
const refusal2 = readServerRefusal(body);
|
|
4467
|
+
throw new MeetError(
|
|
4468
|
+
"transport",
|
|
4469
|
+
refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
|
|
4470
|
+
refusal2?.remedy ?? "You are still in the waiting room. Try again."
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
function hint() {
|
|
4475
|
+
channel?.send(MEET_EVENTS.stateChanged, { v: null });
|
|
4476
|
+
}
|
|
4477
|
+
async function refreshQueue() {
|
|
4478
|
+
const meeting = store.snapshot().meeting;
|
|
4479
|
+
if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
|
|
4480
|
+
try {
|
|
4481
|
+
const attendees = await repository.attendees(meeting.id);
|
|
4482
|
+
store.learn(attendees);
|
|
4483
|
+
for (const attendee of attendees) {
|
|
4484
|
+
const entry = lobbyEntryFrom(attendee);
|
|
4485
|
+
if (entry !== null) store.upsertLobby(entry);
|
|
4486
|
+
else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4487
|
+
}
|
|
4488
|
+
} catch {
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4214
4491
|
return {
|
|
4215
4492
|
watch(roomName) {
|
|
4216
4493
|
channel?.close();
|
|
@@ -4218,58 +4495,22 @@ function createMeetingSession(options) {
|
|
|
4218
4495
|
topic: roomTopic(roomName),
|
|
4219
4496
|
broadcast: [
|
|
4220
4497
|
{
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
{
|
|
4227
|
-
event: MEET_EVENTS.admission,
|
|
4228
|
-
onMessage: (message) => {
|
|
4229
|
-
const verdict = message.data;
|
|
4230
|
-
const decided = asParticipantIdentity(verdict.identity);
|
|
4231
|
-
store.setAdmission(
|
|
4232
|
-
decided,
|
|
4233
|
-
verdict.admitted ? "admitted" : "denied",
|
|
4234
|
-
verdict.by === null ? null : verdict.by
|
|
4235
|
-
);
|
|
4236
|
-
if (decided === knockIdentity()) {
|
|
4237
|
-
if (verdict.admitted) options.onAdmitted?.();
|
|
4238
|
-
else options.onDenied?.();
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
},
|
|
4242
|
-
{
|
|
4243
|
-
event: MEET_EVENTS.lock,
|
|
4244
|
-
onMessage: (message) => {
|
|
4245
|
-
const { locked } = message.data;
|
|
4246
|
-
const meeting = store.snapshot().meeting;
|
|
4247
|
-
store.patch({
|
|
4248
|
-
locked,
|
|
4249
|
-
...meeting !== null ? { meeting: { ...meeting, locked } } : {}
|
|
4250
|
-
});
|
|
4251
|
-
}
|
|
4252
|
-
},
|
|
4253
|
-
{
|
|
4254
|
-
event: MEET_EVENTS.recording,
|
|
4255
|
-
onMessage: (message) => {
|
|
4256
|
-
const payload = message.data;
|
|
4257
|
-
store.setRecording(
|
|
4258
|
-
payload.state,
|
|
4259
|
-
payload.by === null ? null : payload.by,
|
|
4260
|
-
"room"
|
|
4261
|
-
);
|
|
4498
|
+
// 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
|
|
4499
|
+
event: MEET_EVENTS.stateChanged,
|
|
4500
|
+
onMessage: () => {
|
|
4501
|
+
if (store.snapshot().phase === "lobby") void readState2();
|
|
4502
|
+
void refreshQueue();
|
|
4262
4503
|
}
|
|
4263
4504
|
}
|
|
4264
4505
|
],
|
|
4265
|
-
// THE BACKFILL DOOR. A
|
|
4266
|
-
//
|
|
4267
|
-
// re-announce on their own timer.
|
|
4506
|
+
// THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
|
|
4507
|
+
// row, and a waiting tab re-reads its own state.
|
|
4268
4508
|
onBackfill: async () => {
|
|
4269
4509
|
const meeting = store.snapshot().meeting;
|
|
4270
4510
|
if (meeting === null) return;
|
|
4271
4511
|
const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
|
|
4272
4512
|
store.patch({ meeting: fresh, locked: fresh.locked });
|
|
4513
|
+
if (store.snapshot().phase === "lobby") void readState2();
|
|
4273
4514
|
}
|
|
4274
4515
|
});
|
|
4275
4516
|
},
|
|
@@ -4330,7 +4571,7 @@ function createMeetingSession(options) {
|
|
|
4330
4571
|
store.upsertLobby(entry);
|
|
4331
4572
|
return;
|
|
4332
4573
|
}
|
|
4333
|
-
store.setAdmission(attendee.identity, attendee.admissionState, null);
|
|
4574
|
+
store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4334
4575
|
}
|
|
4335
4576
|
},
|
|
4336
4577
|
{
|
|
@@ -4382,62 +4623,75 @@ function createMeetingSession(options) {
|
|
|
4382
4623
|
if (entry !== null) {
|
|
4383
4624
|
store.upsertLobby(entry);
|
|
4384
4625
|
} else {
|
|
4385
|
-
store.setAdmission(attendee.identity, attendee.admissionState, null);
|
|
4626
|
+
store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
|
|
4386
4627
|
}
|
|
4387
4628
|
}
|
|
4388
4629
|
}
|
|
4389
4630
|
});
|
|
4390
4631
|
meetingChannel.requestBackfill("manual");
|
|
4391
4632
|
},
|
|
4392
|
-
knock(roomName) {
|
|
4393
|
-
const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
|
|
4394
|
-
channel = handle;
|
|
4395
|
-
const entry = {
|
|
4396
|
-
identity: knockIdentity(),
|
|
4397
|
-
displayName: identity.displayName,
|
|
4398
|
-
avatarUrl: identity.avatarUrl,
|
|
4399
|
-
userId: identity.userId,
|
|
4400
|
-
state: "knocking",
|
|
4401
|
-
knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4402
|
-
decidedAt: null,
|
|
4403
|
-
decidedBy: null
|
|
4404
|
-
};
|
|
4405
|
-
store.upsertLobby(entry);
|
|
4406
|
-
handle.send(MEET_EVENTS.knock, entry);
|
|
4407
|
-
},
|
|
4408
4633
|
async admit(entry, admitted) {
|
|
4409
4634
|
const by = requireHostUser("meet.admit");
|
|
4410
4635
|
const meeting = requireMeeting("meet.admit");
|
|
4411
|
-
await repository.
|
|
4636
|
+
const row = await repository.decideAdmission({
|
|
4412
4637
|
meetingId: meeting.id,
|
|
4413
4638
|
identity: entry.identity,
|
|
4414
|
-
admitted,
|
|
4639
|
+
decision: admitted ? "admit" : "deny",
|
|
4415
4640
|
byUserId: by
|
|
4416
4641
|
});
|
|
4417
4642
|
store.upsertLobby({
|
|
4418
4643
|
...entry,
|
|
4419
|
-
state:
|
|
4644
|
+
state: decidedState(row.admissionState),
|
|
4420
4645
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4421
4646
|
decidedBy: by
|
|
4422
4647
|
});
|
|
4423
|
-
|
|
4648
|
+
hint();
|
|
4649
|
+
},
|
|
4650
|
+
async undeny(entry) {
|
|
4651
|
+
const by = requireHostUser("meet.undeny");
|
|
4652
|
+
const meeting = requireMeeting("meet.undeny");
|
|
4653
|
+
const row = await repository.decideAdmission({
|
|
4654
|
+
meetingId: meeting.id,
|
|
4424
4655
|
identity: entry.identity,
|
|
4425
|
-
|
|
4426
|
-
by
|
|
4656
|
+
decision: "undeny",
|
|
4657
|
+
byUserId: by
|
|
4427
4658
|
});
|
|
4659
|
+
store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
|
|
4660
|
+
hint();
|
|
4428
4661
|
},
|
|
4429
4662
|
async admitAll() {
|
|
4430
|
-
const
|
|
4431
|
-
|
|
4432
|
-
|
|
4663
|
+
const by = requireHostUser("meet.admitAll");
|
|
4664
|
+
const meeting = requireMeeting("meet.admitAll");
|
|
4665
|
+
const rows = await repository.admitAll(meeting.id, by);
|
|
4666
|
+
for (const row of rows) {
|
|
4667
|
+
store.setAdmission(row.identity, row.admissionState, by);
|
|
4433
4668
|
}
|
|
4669
|
+
hint();
|
|
4670
|
+
},
|
|
4671
|
+
startWaiting() {
|
|
4672
|
+
if (polling) return;
|
|
4673
|
+
polling = true;
|
|
4674
|
+
admittedOnce = false;
|
|
4675
|
+
listen();
|
|
4676
|
+
schedule(0);
|
|
4677
|
+
},
|
|
4678
|
+
stopWaiting,
|
|
4679
|
+
readState: readState2,
|
|
4680
|
+
async withdraw() {
|
|
4681
|
+
stopWaiting();
|
|
4682
|
+
await postLeave("withdraw");
|
|
4683
|
+
patchWaiting({ admission: "withdrawn" });
|
|
4684
|
+
hint();
|
|
4685
|
+
},
|
|
4686
|
+
async askAgain() {
|
|
4687
|
+
await options.rejoin?.();
|
|
4434
4688
|
},
|
|
4435
4689
|
async setLock(locked) {
|
|
4436
4690
|
const by = requireHostUser("meet.setLock");
|
|
4437
4691
|
const meeting = requireMeeting("meet.setLock");
|
|
4438
4692
|
const updated = await repository.setLock(meeting.id, locked, by);
|
|
4439
4693
|
store.patch({ meeting: updated, locked: updated.locked });
|
|
4440
|
-
|
|
4694
|
+
hint();
|
|
4441
4695
|
return updated;
|
|
4442
4696
|
},
|
|
4443
4697
|
/**
|
|
@@ -4495,6 +4749,7 @@ function createMeetingSession(options) {
|
|
|
4495
4749
|
return { endedAt, served: true };
|
|
4496
4750
|
},
|
|
4497
4751
|
dispose() {
|
|
4752
|
+
stopWaiting();
|
|
4498
4753
|
channel?.close();
|
|
4499
4754
|
channel = null;
|
|
4500
4755
|
meetingChannel?.close();
|
|
@@ -4614,7 +4869,6 @@ function kindOf(person) {
|
|
|
4614
4869
|
if (person.isAgent) return "assistant";
|
|
4615
4870
|
if (person.role === "host") return "host";
|
|
4616
4871
|
if (person.role === "cohost") return "cohost";
|
|
4617
|
-
if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
|
|
4618
4872
|
if (person.userId === null) return "guest";
|
|
4619
4873
|
return "member";
|
|
4620
4874
|
}
|
|
@@ -4899,7 +5153,7 @@ function meetingLength(meeting) {
|
|
|
4899
5153
|
if (start === null || end === null || end < start) return null;
|
|
4900
5154
|
return (0, import_format.formatDurationMs)(end - start, { style: "long" });
|
|
4901
5155
|
}
|
|
4902
|
-
async function loadMeetingChat(api, meetingId,
|
|
5156
|
+
async function loadMeetingChat(api, meetingId, meetingPass = null) {
|
|
4903
5157
|
const operation = "meet.loadMeetingChat";
|
|
4904
5158
|
let response;
|
|
4905
5159
|
try {
|
|
@@ -4907,8 +5161,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
|
|
|
4907
5161
|
path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
|
|
4908
5162
|
method: "GET",
|
|
4909
5163
|
operation,
|
|
4910
|
-
requireAuth:
|
|
4911
|
-
...
|
|
5164
|
+
requireAuth: meetingPass === null,
|
|
5165
|
+
...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
|
|
4912
5166
|
});
|
|
4913
5167
|
} catch (error) {
|
|
4914
5168
|
return {
|
|
@@ -5052,7 +5306,12 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
|
|
|
5052
5306
|
// src/core/room.ts
|
|
5053
5307
|
var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
|
|
5054
5308
|
var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
|
|
5055
|
-
var
|
|
5309
|
+
var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
|
|
5310
|
+
var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
|
|
5311
|
+
function parseStanding(value) {
|
|
5312
|
+
return KNOWN_STANDINGS.includes(value) ? value : null;
|
|
5313
|
+
}
|
|
5314
|
+
var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
|
|
5056
5315
|
function parseRole(value) {
|
|
5057
5316
|
return KNOWN_ROLES.includes(value) ? value : "participant";
|
|
5058
5317
|
}
|
|
@@ -5086,6 +5345,9 @@ function projectParticipant(driver, joinedAt) {
|
|
|
5086
5345
|
// ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
|
|
5087
5346
|
// server's; the attribute is simply the one that always arrives.
|
|
5088
5347
|
role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
|
|
5348
|
+
standing: parseStanding(
|
|
5349
|
+
driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
|
|
5350
|
+
),
|
|
5089
5351
|
canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
|
|
5090
5352
|
isLocal: driver.isLocal,
|
|
5091
5353
|
isAgent: metadata.is_agent === true,
|
|
@@ -5158,7 +5420,7 @@ function createRoomEngine(options) {
|
|
|
5158
5420
|
const normalized = normalizeMeetError(error, operation);
|
|
5159
5421
|
store.patch({
|
|
5160
5422
|
phase: "failed",
|
|
5161
|
-
failure: { message: normalized.message, remedy: normalized.remedy }
|
|
5423
|
+
failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
|
|
5162
5424
|
});
|
|
5163
5425
|
report(
|
|
5164
5426
|
normalized.code === "session-unavailable" ? "warn" : "error",
|
|
@@ -5348,7 +5610,7 @@ function createRoomEngine(options) {
|
|
|
5348
5610
|
}
|
|
5349
5611
|
};
|
|
5350
5612
|
async function connect(request) {
|
|
5351
|
-
store.patch({ phase: "requesting-token", failure: null });
|
|
5613
|
+
store.patch({ phase: "requesting-token", failure: null, waiting: null });
|
|
5352
5614
|
const token = await tokens.get({
|
|
5353
5615
|
roomName: request.roomName,
|
|
5354
5616
|
identity,
|
|
@@ -5362,16 +5624,27 @@ function createRoomEngine(options) {
|
|
|
5362
5624
|
breakoutRoomId: token.breakoutRoomId ?? null,
|
|
5363
5625
|
grants: {
|
|
5364
5626
|
role: token.role,
|
|
5627
|
+
standing: token.standing,
|
|
5365
5628
|
canRecord: token.canRecord,
|
|
5366
5629
|
canPublish: token.canPublish,
|
|
5367
5630
|
canSubscribe: token.canSubscribe
|
|
5368
5631
|
}
|
|
5369
5632
|
});
|
|
5370
|
-
if (token.lobby) {
|
|
5371
|
-
store.patch({
|
|
5633
|
+
if (token.lobby || !token.canSubscribe) {
|
|
5634
|
+
store.patch({
|
|
5635
|
+
phase: "lobby",
|
|
5636
|
+
waiting: {
|
|
5637
|
+
admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
|
|
5638
|
+
admittersPresent: null,
|
|
5639
|
+
reknockAllowed: true,
|
|
5640
|
+
knockExpiresAt: null,
|
|
5641
|
+
pollSeconds: token.pollSeconds ?? 3,
|
|
5642
|
+
liveVersion: 0
|
|
5643
|
+
}
|
|
5644
|
+
});
|
|
5372
5645
|
return;
|
|
5373
5646
|
}
|
|
5374
|
-
store.patch({ phase: "connecting" });
|
|
5647
|
+
store.patch({ phase: "connecting", waiting: null });
|
|
5375
5648
|
const created = await options.driverFactory(handlers);
|
|
5376
5649
|
if (disposed) {
|
|
5377
5650
|
await created.disconnect();
|
|
@@ -5707,7 +5980,7 @@ function createInRoomClient(options) {
|
|
|
5707
5980
|
}
|
|
5708
5981
|
function headers() {
|
|
5709
5982
|
const token = options.resolveRoomToken();
|
|
5710
|
-
return token === null ? void 0 : { [
|
|
5983
|
+
return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
|
|
5711
5984
|
}
|
|
5712
5985
|
function refusal2(response, operation, remedy) {
|
|
5713
5986
|
return refusalFromResponse(response, operation, remedy);
|
|
@@ -6188,7 +6461,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
|
|
|
6188
6461
|
async function loadGuestMeetingRecord(args) {
|
|
6189
6462
|
const { api, repository, meetingId, meeting, roomToken } = args;
|
|
6190
6463
|
const session = args.session === true;
|
|
6191
|
-
if (roomToken
|
|
6464
|
+
if (roomToken?.meetingPass == null && !session) {
|
|
6192
6465
|
return refusedBundle(
|
|
6193
6466
|
meeting,
|
|
6194
6467
|
"This meeting's record is only readable by the people who were in it.",
|
|
@@ -6198,12 +6471,12 @@ async function loadGuestMeetingRecord(args) {
|
|
|
6198
6471
|
let response;
|
|
6199
6472
|
try {
|
|
6200
6473
|
response = await api.request({
|
|
6201
|
-
path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}
|
|
6474
|
+
path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
|
|
6202
6475
|
method: "GET",
|
|
6203
6476
|
// A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
|
|
6204
6477
|
requireAuth: session,
|
|
6205
6478
|
operation: "meet.record",
|
|
6206
|
-
headers: roomToken
|
|
6479
|
+
headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
|
|
6207
6480
|
});
|
|
6208
6481
|
} catch (error) {
|
|
6209
6482
|
return refusedBundle(
|
|
@@ -6591,6 +6864,10 @@ function takeMoveRequest(store) {
|
|
|
6591
6864
|
}
|
|
6592
6865
|
|
|
6593
6866
|
// src/core/observation.ts
|
|
6867
|
+
function observedRole(role, standing) {
|
|
6868
|
+
if (role === "host" || role === "cohost") return role;
|
|
6869
|
+
return isLinkFollower(standing) ? "guest" : "participant";
|
|
6870
|
+
}
|
|
6594
6871
|
function observedDevice(track) {
|
|
6595
6872
|
if (track.enabled) return "on";
|
|
6596
6873
|
const refused = track.unavailable;
|
|
@@ -6618,13 +6895,15 @@ function observedPhase(input) {
|
|
|
6618
6895
|
const raw = snapshot.phase;
|
|
6619
6896
|
const live = raw === "connected" || raw === "reconnecting";
|
|
6620
6897
|
if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
|
|
6898
|
+
const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
|
|
6899
|
+
if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
|
|
6621
6900
|
switch (raw) {
|
|
6622
6901
|
case "idle":
|
|
6623
6902
|
case "failed":
|
|
6624
6903
|
case "left":
|
|
6625
6904
|
return "prejoin";
|
|
6626
6905
|
case "lobby":
|
|
6627
|
-
return
|
|
6906
|
+
return waitingPhase(snapshot);
|
|
6628
6907
|
case "requesting-token":
|
|
6629
6908
|
case "connecting":
|
|
6630
6909
|
return "joining";
|
|
@@ -6633,20 +6912,39 @@ function observedPhase(input) {
|
|
|
6633
6912
|
return "in_call";
|
|
6634
6913
|
}
|
|
6635
6914
|
}
|
|
6915
|
+
function waitingPhase(snapshot) {
|
|
6916
|
+
switch (snapshot.waiting?.admission) {
|
|
6917
|
+
case "waiting_host":
|
|
6918
|
+
return "waiting:host_not_started";
|
|
6919
|
+
case "denied":
|
|
6920
|
+
return "denied";
|
|
6921
|
+
case "expired":
|
|
6922
|
+
return "knock_expired";
|
|
6923
|
+
case "withdrawn":
|
|
6924
|
+
return "left";
|
|
6925
|
+
case "ended":
|
|
6926
|
+
return "ended";
|
|
6927
|
+
case "removed":
|
|
6928
|
+
return "refused:removed";
|
|
6929
|
+
default:
|
|
6930
|
+
return "waiting:knocking";
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6636
6933
|
var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
|
|
6637
6934
|
function observe(input) {
|
|
6638
6935
|
const { snapshot } = input;
|
|
6639
6936
|
const phase = observedPhase(input);
|
|
6640
6937
|
const inCall = phase === "in_call";
|
|
6641
|
-
const
|
|
6642
|
-
const canAdmit =
|
|
6938
|
+
const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
|
|
6939
|
+
const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
|
|
6940
|
+
const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
|
|
6643
6941
|
const deviceInPlay = phase === "joining" || inCall;
|
|
6644
6942
|
return {
|
|
6645
6943
|
phase,
|
|
6646
|
-
reason: null,
|
|
6944
|
+
reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
|
|
6647
6945
|
role,
|
|
6648
6946
|
connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
|
|
6649
|
-
hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
|
|
6947
|
+
hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
|
|
6650
6948
|
lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
|
|
6651
6949
|
recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
|
|
6652
6950
|
audioBlocked: null,
|
|
@@ -6709,7 +7007,7 @@ function participantAttributes(p) {
|
|
|
6709
7007
|
"data-meet-participant": "",
|
|
6710
7008
|
"data-meet-participant-identity": p.identity,
|
|
6711
7009
|
"data-meet-participant-name": p.displayName,
|
|
6712
|
-
"data-meet-participant-role": p.role,
|
|
7010
|
+
"data-meet-participant-role": observedRole(p.role, p.standing),
|
|
6713
7011
|
"data-meet-participant-self": String(p.isLocal)
|
|
6714
7012
|
};
|
|
6715
7013
|
const quality = observedQuality(p.quality);
|