openvibers/openvibe.chat Commits
Workspace rooms show a shared screen: a Bot screen share link, live above the conversation for members only (watch, or click and type with a use link) (#164)
Changes
@@@ -403,6 +403,11 @@ member on their first visit, and nobody else reads it, whatever role they once h(`/r/:slug/tasks/:id`: details, the full result, sources, cost). Open pages refresh the panel when the chat socketsends `room_task`.- **Cancelling** is the asker's or a moderator's. Limits: 20 tasks an hour per person, 300 a day per room.++- **A shared screen** (migration `0028_room_screen.sql`): a room's managers set a screen share link from OpenVibe.Bot++ (`https://openvibe.bot/screen/…`, made by the computer's owner on its screen page; nothing else is accepted). Members++ see that computer's screen live above the conversation, and with a "use" link they can start it and click and type on++ it (at the level the owner set on the computer). The link is a key to the screen, so only members get it (the page,++ and `screen.url` in `GET /api/chat/rooms/:slug`). Pages allow `frame-src https://openvibe.bot`.- **Account data:** a person's tasks are in their export (`room_tasks.json`); on deletion the room keeps the workwithout who asked or who took it.nnew file mode 100644@@@ -0,0 +1,5 @@++-- phase: expand++-- A workspace room's shared screen (owner 2026-10-25: a screen the group watches and uses together): a screen share++-- link from OpenVibe.Bot (https://openvibe.bot/screen/<token>, made by the computer's owner), shown beside the++-- conversation to the room's members only. The link is a key to that screen, so only members ever see it.++ALTER TABLE rooms ADD COLUMN IF NOT EXISTS screen_url text;@@@ -357,3 +357,7 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }@keyframes oc-work { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }@media (prefers-reduced-motion: reduce) { .oc-task-running .oc-task-state { animation: none; } }@media (max-width: 900px) { .oc-ws { grid-template-columns: minmax(0, 1fr); } .oc-queue { position: static; max-height: none; order: -1; } }++/* A workspace room's shared screen (rooms.screen_url, a screen share page from OpenVibe.Bot), above the conversation. */++.oc-screen { margin: 10px 0 14px; border: 1px solid var(--oc-line); border-radius: 12px; background: var(--oc-panel); overflow: hidden; }++.oc-screen summary { cursor: pointer; padding: 8px 12px; font-weight: 600; }++.oc-screen iframe { display: block; width: 100%; aspect-ratio: 16 / 10; min-height: 260px; max-height: 78vh; border: 0; background: #000; }@@@ -383,7 +383,16 @@ async function setRole(room, actor, targetUserId, role) {return role;}--async function update(room, actor, { name, topic, visibility, slow_seconds, join_role } = {}, { fromServer = false, minName = 3 } = {}) {++/** A screen share link from OpenVibe.Bot (screen_url), or null to clear; anything else is refused. */++const SCREEN_RE = /^https:\/\/openvibe\.bot\/screen\/[A-Za-z0-9_-]{40,60}$/;++function screenUrlOf(v) {++ const s = String(v == null ? '' : v).trim();++ if (!s) return null;++ if (!SCREEN_RE.test(s)) fail(422, 'rooms.screen_url', 'A shared screen is a link from OpenVibe.Bot: https://openvibe.bot/screen/…');++ return s;++}++++async function update(room, actor, { name, topic, visibility, slow_seconds, join_role, screen_url } = {}, { fromServer = false, minName = 3 } = {}) {if (!(await access(room, actor)).manage) fail(403, 'rooms.not_owner', 'Only the owner changes the room');// A server's channel follows the server's visibility, and is changed through the server (servers.js).if (room.server_id && !fromServer && visibility != null && visibility !== room.visibility) fail(409, 'rooms.server_channel', 'A channel is as public as its server: change the server instead');@@@ -397,8 +406,12 @@ async function update(room, actor, { name, topic, visibility, slow_seconds, joinif (kindOf(room) !== 'call' || !JOIN_ROLES_CALL.includes(join_role)) fail(422, 'rooms.join_role', 'People join a call room as speakers, participants or viewers');next.join_role = join_role;}++ if (screen_url !== undefined) {++ if (!isWorkspace(room)) fail(409, 'rooms.not_workspace', 'Only a workspace room shows a shared screen');++ next.screen_url = screenUrlOf(screen_url);++ }await db.tx(async () => {-- await db.run('UPDATE rooms SET name = ?, topic = ?, visibility = ?, slow_seconds = ?, join_role = ? WHERE id = ?', [next.name, next.topic, next.visibility, next.slow_seconds, next.join_role == null ? null : next.join_role, room.id]);++ await db.run('UPDATE rooms SET name = ?, topic = ?, visibility = ?, slow_seconds = ?, join_role = ?, screen_url = ? WHERE id = ?', [next.name, next.topic, next.visibility, next.slow_seconds, next.join_role == null ? null : next.join_role, next.screen_url == null ? null : next.screen_url, room.id]);// Public → private: everything the room published stops being replayable on the realtime plane.if (room.visibility === 'public' && next.visibility === 'private') {await announceDeleted(room, (await db.all('SELECT id FROM room_messages WHERE room_id = ? AND is_deleted = 0 ORDER BY id', [room.id])).map((r) => r.id));@@@ -120,6 +120,8 @@ router.get('/:slug', optionalAuth, async (req, res) => {res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({room: await rooms.publicRoom(room, { role: a.role }),can: { read: a.read, post: a.post, join: a.join, talk: a.talk, moderate: a.moderate, manage: a.manage },++ // A workspace room's shared screen link is a key to that screen: members only.++ ...(rooms.isWorkspace(room) && a.role && room.screen_url ? { screen: { url: room.screen_url } } : {}),...(room.kind === 'call' ? { call: callOf(room) } : {}),});});@@@ -84,6 +84,8 @@ function contentSecurityPolicy(nonce) {"media-src 'self' blob: https:","font-src 'self' data:","connect-src 'self' wss://openvibe.chat https://openvibe.network wss://openvibe.network https://openvibe.events wss://openvibe.events https://cloudflareinsights.com",++ // A workspace room's shared screen: a screen share page from OpenVibe.Bot (rooms.screen_url).++ "frame-src 'self' https://openvibe.bot","frame-ancestors 'self'","base-uri 'self'","object-src 'none'",@@@ -710,6 +710,7 @@ ${people}${aiHere}${error}${isCall ? callPanel(room, can, me) : ''}${feed ? `<div class="fv oc-room-fv" data-feed="${esc(feed.id)}"></div>` : ''}++${work && room.screen_url && can.role ? `<details class="oc-screen" open><summary>Shared screen</summary><iframe src="${esc(room.screen_url)}" title="The room's shared screen" loading="lazy" referrerpolicy="no-referrer" allow="clipboard-write; fullscreen"></iframe></details>` : ''}${work ? '<div class="oc-ws"><div class="oc-ws-main">' : ''}<ol class="oc-feed" id="oc-feed" aria-live="polite" aria-label="Messages">${msgs.map((m) => roomMessage(m, can, room.slug, me)).join('\n') || `<li class="oc-empty oc-muted">${room.kind === 'system' ? 'No announcements yet.' : 'No messages yet. Say hello.'}</li>`}</ol>${composer}${tools}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,});@@@ -835,6 +836,7 @@ ${can.manage ? `<form class="oc-form" method="post" action="/r/${esc(room.slug)}<label>Who can read <select name="visibility"><option value="public"${room.visibility === 'public' ? ' selected' : ''}>Anyone (public)</option><option value="private"${room.visibility === 'private' ? ' selected' : ''}>Members only (private)</option></select></label>${room.kind === 'call' ? `<label>People who join <select name="join_role">${rooms.JOIN_ROLES_CALL.map((r) => `<option value="${r}"${r === addRole ? ' selected' : ''}>${{ speaker: 'can talk (speakers)', participant: 'listen and write (participants)', viewer: 'listen and read (viewers)' }[r]}</option>`).join('')}</select></label>` : ''}<label>Slow mode (seconds between messages, 0 = off) <input name="slow_seconds" type="number" min="0" max="600" value="${Number(room.slow_seconds) || 0}"></label>++${rooms.isWorkspace(room) ? `<label>Shared screen <input name="screen_url" type="url" value="${esc(room.screen_url || '')}" placeholder="https://openvibe.bot/screen/…"> <span class="oc-muted">A screen share link from OpenVibe.Bot (a computer's owner makes it on its screen page). Members see the screen beside the conversation, and with a "use" link they can click and type on it. Empty: no screen.</span></label>` : ''}<p><button type="submit">Save</button></p></form>` : ''}<h2>Members</h2><p class="oc-muted">${esc(roleHelp(room.kind))}</p>@@@ -857,7 +859,8 @@ ${attachList}`,if (!room || !(await rooms.access(room, actor.user)).read) return res.redirect(303, '/rooms');req.roomBack = `/r/${room.slug}/settings`;const b = req.body || {};-- await rooms.update(room, actor.user, { name: b.name, topic: b.topic, visibility: b.visibility, slow_seconds: b.slow_seconds === undefined ? undefined : Number(b.slow_seconds), join_role: room.kind === 'call' ? b.join_role : undefined });++ await rooms.update(room, actor.user, { name: b.name, topic: b.topic, visibility: b.visibility, slow_seconds: b.slow_seconds === undefined ? undefined : Number(b.slow_seconds), join_role: room.kind === 'call' ? b.join_role : undefined,++ ...(rooms.isWorkspace(room) && b.screen_url !== undefined ? { screen_url: b.screen_url } : {}) });await applyRoomChange(room);res.redirect(303, `/r/${room.slug}/settings?saved=1`);}));@@@ -152,4 +152,20 @@ t('the room page shows the queue beside the conversation; the task page has theassert.strictEqual((await h.http('GET', `/r/staff/tasks/${id}`, { headers: { cookie: `ov_token=${user.token}` } })).status, 404);});++t('a workspace room shows its shared screen to members only; only a Bot screen link is taken', async () => {++ const link = 'https://openvibe.bot/screen/' + 'a'.repeat(43);++ let r = await api('PATCH', '/rooms/staff', staff, { screen_url: 'https://evil.test/screen/x' });++ assert.strictEqual(r.status, 422);++ r = await api('PATCH', '/rooms/staff', staff, { screen_url: link });++ assert.strictEqual(r.status, 200, r.text);++ r = await api('GET', '/rooms/staff', staff2);++ assert.strictEqual(r.body.screen.url, link);++ const page = await h.http('GET', '/r/staff', { headers: { cookie: `ov_token=${staff.token}` } });++ assert.match(page.text, new RegExp(`<iframe src="${link.replace(/\//g, '\\/')}"`));++ assert.match(page.headers.get('content-security-policy'), /frame-src 'self' https:\/\/openvibe\.bot/);++ assert.strictEqual((await api('GET', '/rooms/staff', user)).status, 404, 'others never see the link');++ assert.strictEqual((await api('PATCH', '/rooms/staff', staff, { screen_url: '' })).status, 200);++ assert.ok(!(await api('GET', '/rooms/staff', staff)).body.screen, 'cleared');++});++t.run(async () => { if (h && h.close) await h.close(); });
@@ -403,6 +403,11 @@ member on their first visit, and nobody else reads it, whatever role they once h
@@ -403,6 +403,11 @@ member on their first visit, and nobody else reads it, whatever role they once h
(`/r/:slug/tasks/:id`: details, the full result, sources, cost). Open pages refresh the panel when the chat socket
(`/r/:slug/tasks/:id`: details, the full result, sources, cost). Open pages refresh the panel when the chat socket
sends `room_task`.
sends `room_task`.
- **Cancelling** is the asker's or a moderator's. Limits: 20 tasks an hour per person, 300 a day per room.
- **Cancelling** is the asker's or a moderator's. Limits: 20 tasks an hour per person, 300 a day per room.
+- **A shared screen** (migration `0028_room_screen.sql`): a room's managers set a screen share link from OpenVibe.Bot
+ (`https://openvibe.bot/screen/…`, made by the computer's owner on its screen page; nothing else is accepted). Members
+ see that computer's screen live above the conversation, and with a "use" link they can start it and click and type on
+ it (at the level the owner set on the computer). The link is a key to the screen, so only members get it (the page,
+ and `screen.url` in `GET /api/chat/rooms/:slug`). Pages allow `frame-src https://openvibe.bot`.
- **Account data:** a person's tasks are in their export (`room_tasks.json`); on deletion the room keeps the work
- **Account data:** a person's tasks are in their export (`room_tasks.json`); on deletion the room keeps the work
without who asked or who took it.
without who asked or who took it.
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+-- phase: expand
+-- A workspace room's shared screen (owner 2026-10-25: a screen the group watches and uses together): a screen share
+-- link from OpenVibe.Bot (https://openvibe.bot/screen/<token>, made by the computer's owner), shown beside the
+-- conversation to the room's members only. The link is a key to that screen, so only members ever see it.
+ALTER TABLE rooms ADD COLUMN IF NOT EXISTS screen_url text;
@@ -357,3 +357,7 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }
@@ -357,3 +357,7 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }
@keyframes oc-work { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@keyframes oc-work { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .oc-task-running .oc-task-state { animation: none; } }
@media (prefers-reduced-motion: reduce) { .oc-task-running .oc-task-state { animation: none; } }
@media (max-width: 900px) { .oc-ws { grid-template-columns: minmax(0, 1fr); } .oc-queue { position: static; max-height: none; order: -1; } }
@media (max-width: 900px) { .oc-ws { grid-template-columns: minmax(0, 1fr); } .oc-queue { position: static; max-height: none; order: -1; } }
+/* A workspace room's shared screen (rooms.screen_url, a screen share page from OpenVibe.Bot), above the conversation. */
+.oc-screen { margin: 10px 0 14px; border: 1px solid var(--oc-line); border-radius: 12px; background: var(--oc-panel); overflow: hidden; }
+.oc-screen summary { cursor: pointer; padding: 8px 12px; font-weight: 600; }
+.oc-screen iframe { display: block; width: 100%; aspect-ratio: 16 / 10; min-height: 260px; max-height: 78vh; border: 0; background: #000; }
@@ -383,7 +383,16 @@ async function setRole(room, actor, targetUserId, role) {
@@ -383,7 +383,16 @@ async function setRole(room, actor, targetUserId, role) {
return role;
return role;
}
}
-async function update(room, actor, { name, topic, visibility, slow_seconds, join_role } = {}, { fromServer = false, minName = 3 } = {}) {
+/** A screen share link from OpenVibe.Bot (screen_url), or null to clear; anything else is refused. */
+const SCREEN_RE = /^https:\/\/openvibe\.bot\/screen\/[A-Za-z0-9_-]{40,60}$/;
+function screenUrlOf(v) {
+ const s = String(v == null ? '' : v).trim();
+ if (!s) return null;
+ if (!SCREEN_RE.test(s)) fail(422, 'rooms.screen_url', 'A shared screen is a link from OpenVibe.Bot: https://openvibe.bot/screen/…');
+ return s;
+}
+
+async function update(room, actor, { name, topic, visibility, slow_seconds, join_role, screen_url } = {}, { fromServer = false, minName = 3 } = {}) {
if (!(await access(room, actor)).manage) fail(403, 'rooms.not_owner', 'Only the owner changes the room');
if (!(await access(room, actor)).manage) fail(403, 'rooms.not_owner', 'Only the owner changes the room');
// A server's channel follows the server's visibility, and is changed through the server (servers.js).
// A server's channel follows the server's visibility, and is changed through the server (servers.js).
if (room.server_id && !fromServer && visibility != null && visibility !== room.visibility) fail(409, 'rooms.server_channel', 'A channel is as public as its server: change the server instead');
if (room.server_id && !fromServer && visibility != null && visibility !== room.visibility) fail(409, 'rooms.server_channel', 'A channel is as public as its server: change the server instead');
@@ -397,8 +406,12 @@ async function update(room, actor, { name, topic, visibility, slow_seconds, join
@@ -397,8 +406,12 @@ async function update(room, actor, { name, topic, visibility, slow_seconds, join
if (kindOf(room) !== 'call' || !JOIN_ROLES_CALL.includes(join_role)) fail(422, 'rooms.join_role', 'People join a call room as speakers, participants or viewers');
if (kindOf(room) !== 'call' || !JOIN_ROLES_CALL.includes(join_role)) fail(422, 'rooms.join_role', 'People join a call room as speakers, participants or viewers');
next.join_role = join_role;
next.join_role = join_role;
}
}
+ if (screen_url !== undefined) {
+ if (!isWorkspace(room)) fail(409, 'rooms.not_workspace', 'Only a workspace room shows a shared screen');
+ next.screen_url = screenUrlOf(screen_url);
+ }
await db.tx(async () => {
await db.tx(async () => {
- await db.run('UPDATE rooms SET name = ?, topic = ?, visibility = ?, slow_seconds = ?, join_role = ? WHERE id = ?', [next.name, next.topic, next.visibility, next.slow_seconds, next.join_role == null ? null : next.join_role, room.id]);
+ await db.run('UPDATE rooms SET name = ?, topic = ?, visibility = ?, slow_seconds = ?, join_role = ?, screen_url = ? WHERE id = ?', [next.name, next.topic, next.visibility, next.slow_seconds, next.join_role == null ? null : next.join_role, next.screen_url == null ? null : next.screen_url, room.id]);
// Public → private: everything the room published stops being replayable on the realtime plane.
// Public → private: everything the room published stops being replayable on the realtime plane.
if (room.visibility === 'public' && next.visibility === 'private') {
if (room.visibility === 'public' && next.visibility === 'private') {
await announceDeleted(room, (await db.all('SELECT id FROM room_messages WHERE room_id = ? AND is_deleted = 0 ORDER BY id', [room.id])).map((r) => r.id));
await announceDeleted(room, (await db.all('SELECT id FROM room_messages WHERE room_id = ? AND is_deleted = 0 ORDER BY id', [room.id])).map((r) => r.id));
@@ -120,6 +120,8 @@ router.get('/:slug', optionalAuth, async (req, res) => {
@@ -120,6 +120,8 @@ router.get('/:slug', optionalAuth, async (req, res) => {
res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({
res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({
room: await rooms.publicRoom(room, { role: a.role }),
room: await rooms.publicRoom(room, { role: a.role }),
can: { read: a.read, post: a.post, join: a.join, talk: a.talk, moderate: a.moderate, manage: a.manage },
can: { read: a.read, post: a.post, join: a.join, talk: a.talk, moderate: a.moderate, manage: a.manage },
+ // A workspace room's shared screen link is a key to that screen: members only.
+ ...(rooms.isWorkspace(room) && a.role && room.screen_url ? { screen: { url: room.screen_url } } : {}),
...(room.kind === 'call' ? { call: callOf(room) } : {}),
...(room.kind === 'call' ? { call: callOf(room) } : {}),
});
});
});
});
@@ -84,6 +84,8 @@ function contentSecurityPolicy(nonce) {
@@ -84,6 +84,8 @@ function contentSecurityPolicy(nonce) {
"media-src 'self' blob: https:",
"media-src 'self' blob: https:",
"font-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self' wss://openvibe.chat https://openvibe.network wss://openvibe.network https://openvibe.events wss://openvibe.events https://cloudflareinsights.com",
"connect-src 'self' wss://openvibe.chat https://openvibe.network wss://openvibe.network https://openvibe.events wss://openvibe.events https://cloudflareinsights.com",
+ // A workspace room's shared screen: a screen share page from OpenVibe.Bot (rooms.screen_url).
+ "frame-src 'self' https://openvibe.bot",
"frame-ancestors 'self'",
"frame-ancestors 'self'",
"base-uri 'self'",
"base-uri 'self'",
"object-src 'none'",
"object-src 'none'",
@@ -710,6 +710,7 @@ ${people}
@@ -710,6 +710,7 @@ ${people}
${aiHere}${error}
${aiHere}${error}
${isCall ? callPanel(room, can, me) : ''}
${isCall ? callPanel(room, can, me) : ''}
${feed ? `<div class="fv oc-room-fv" data-feed="${esc(feed.id)}"></div>` : ''}
${feed ? `<div class="fv oc-room-fv" data-feed="${esc(feed.id)}"></div>` : ''}
+${work && room.screen_url && can.role ? `<details class="oc-screen" open><summary>Shared screen</summary><iframe src="${esc(room.screen_url)}" title="The room's shared screen" loading="lazy" referrerpolicy="no-referrer" allow="clipboard-write; fullscreen"></iframe></details>` : ''}
${work ? '<div class="oc-ws"><div class="oc-ws-main">' : ''}<ol class="oc-feed" id="oc-feed" aria-live="polite" aria-label="Messages">${msgs.map((m) => roomMessage(m, can, room.slug, me)).join('\n') || `<li class="oc-empty oc-muted">${room.kind === 'system' ? 'No announcements yet.' : 'No messages yet. Say hello.'}</li>`}</ol>
${work ? '<div class="oc-ws"><div class="oc-ws-main">' : ''}<ol class="oc-feed" id="oc-feed" aria-live="polite" aria-label="Messages">${msgs.map((m) => roomMessage(m, can, room.slug, me)).join('\n') || `<li class="oc-empty oc-muted">${room.kind === 'system' ? 'No announcements yet.' : 'No messages yet. Say hello.'}</li>`}</ol>
${composer}${tools}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,
${composer}${tools}${work ? `</div>${queuePanel(room, can, me, work)}</div>` : ''}`,
});
});
@@ -835,6 +836,7 @@ ${can.manage ? `<form class="oc-form" method="post" action="/r/${esc(room.slug)}
@@ -835,6 +836,7 @@ ${can.manage ? `<form class="oc-form" method="post" action="/r/${esc(room.slug)}
<label>Who can read <select name="visibility"><option value="public"${room.visibility === 'public' ? ' selected' : ''}>Anyone (public)</option><option value="private"${room.visibility === 'private' ? ' selected' : ''}>Members only (private)</option></select></label>
<label>Who can read <select name="visibility"><option value="public"${room.visibility === 'public' ? ' selected' : ''}>Anyone (public)</option><option value="private"${room.visibility === 'private' ? ' selected' : ''}>Members only (private)</option></select></label>
${room.kind === 'call' ? `<label>People who join <select name="join_role">${rooms.JOIN_ROLES_CALL.map((r) => `<option value="${r}"${r === addRole ? ' selected' : ''}>${{ speaker: 'can talk (speakers)', participant: 'listen and write (participants)', viewer: 'listen and read (viewers)' }[r]}</option>`).join('')}</select></label>` : ''}
${room.kind === 'call' ? `<label>People who join <select name="join_role">${rooms.JOIN_ROLES_CALL.map((r) => `<option value="${r}"${r === addRole ? ' selected' : ''}>${{ speaker: 'can talk (speakers)', participant: 'listen and write (participants)', viewer: 'listen and read (viewers)' }[r]}</option>`).join('')}</select></label>` : ''}
<label>Slow mode (seconds between messages, 0 = off) <input name="slow_seconds" type="number" min="0" max="600" value="${Number(room.slow_seconds) || 0}"></label>
<label>Slow mode (seconds between messages, 0 = off) <input name="slow_seconds" type="number" min="0" max="600" value="${Number(room.slow_seconds) || 0}"></label>
+${rooms.isWorkspace(room) ? `<label>Shared screen <input name="screen_url" type="url" value="${esc(room.screen_url || '')}" placeholder="https://openvibe.bot/screen/…"> <span class="oc-muted">A screen share link from OpenVibe.Bot (a computer's owner makes it on its screen page). Members see the screen beside the conversation, and with a "use" link they can click and type on it. Empty: no screen.</span></label>` : ''}
<p><button type="submit">Save</button></p></form>` : ''}
<p><button type="submit">Save</button></p></form>` : ''}
<h2>Members</h2>
<h2>Members</h2>
<p class="oc-muted">${esc(roleHelp(room.kind))}</p>
<p class="oc-muted">${esc(roleHelp(room.kind))}</p>
@@ -857,7 +859,8 @@ ${attachList}`,
@@ -857,7 +859,8 @@ ${attachList}`,
if (!room || !(await rooms.access(room, actor.user)).read) return res.redirect(303, '/rooms');
if (!room || !(await rooms.access(room, actor.user)).read) return res.redirect(303, '/rooms');
req.roomBack = `/r/${room.slug}/settings`;
req.roomBack = `/r/${room.slug}/settings`;
const b = req.body || {};
const b = req.body || {};
- await rooms.update(room, actor.user, { name: b.name, topic: b.topic, visibility: b.visibility, slow_seconds: b.slow_seconds === undefined ? undefined : Number(b.slow_seconds), join_role: room.kind === 'call' ? b.join_role : undefined });
+ await rooms.update(room, actor.user, { name: b.name, topic: b.topic, visibility: b.visibility, slow_seconds: b.slow_seconds === undefined ? undefined : Number(b.slow_seconds), join_role: room.kind === 'call' ? b.join_role : undefined,
+ ...(rooms.isWorkspace(room) && b.screen_url !== undefined ? { screen_url: b.screen_url } : {}) });
await applyRoomChange(room);
await applyRoomChange(room);
res.redirect(303, `/r/${room.slug}/settings?saved=1`);
res.redirect(303, `/r/${room.slug}/settings?saved=1`);
}));
}));
@@ -152,4 +152,20 @@ t('the room page shows the queue beside the conversation; the task page has the
@@ -152,4 +152,20 @@ t('the room page shows the queue beside the conversation; the task page has the
assert.strictEqual((await h.http('GET', `/r/staff/tasks/${id}`, { headers: { cookie: `ov_token=${user.token}` } })).status, 404);
assert.strictEqual((await h.http('GET', `/r/staff/tasks/${id}`, { headers: { cookie: `ov_token=${user.token}` } })).status, 404);
});
});
+t('a workspace room shows its shared screen to members only; only a Bot screen link is taken', async () => {
+ const link = 'https://openvibe.bot/screen/' + 'a'.repeat(43);
+ let r = await api('PATCH', '/rooms/staff', staff, { screen_url: 'https://evil.test/screen/x' });
+ assert.strictEqual(r.status, 422);
+ r = await api('PATCH', '/rooms/staff', staff, { screen_url: link });
+ assert.strictEqual(r.status, 200, r.text);
+ r = await api('GET', '/rooms/staff', staff2);
+ assert.strictEqual(r.body.screen.url, link);
+ const page = await h.http('GET', '/r/staff', { headers: { cookie: `ov_token=${staff.token}` } });
+ assert.match(page.text, new RegExp(`<iframe src="${link.replace(/\//g, '\\/')}"`));
+ assert.match(page.headers.get('content-security-policy'), /frame-src 'self' https:\/\/openvibe\.bot/);
+ assert.strictEqual((await api('GET', '/rooms/staff', user)).status, 404, 'others never see the link');
+ assert.strictEqual((await api('PATCH', '/rooms/staff', staff, { screen_url: '' })).status, 200);
+ assert.ok(!(await api('GET', '/rooms/staff', staff)).body.screen, 'cleared');
+});
+
t.run(async () => { if (h && h.close) await h.close(); });
t.run(async () => { if (h && h.close) await h.close(); });