openvibers/openvibe.live Commits
Agent streams: the person's own streams, for the Dock's agent view (K-D3) (#632)
Changes
@@@ -3,6 +3,7 @@* /api/streams/agent — the agent streams API (live.agent-stream@1 and the publish/view routes around it).** POST / create the stream and its publish ticket { stream, publish } 201++ * GET / the caller's own streams, the live one first { streams } (Dock K-D3)* GET /:id the stream (view rule), plus what its page needs* PUT /:id/visibility the owner's switch* POST /:id/rotate-link a fresh unlisted link secret (the old links stop working)@@@ -161,6 +162,28 @@ async function watchable(req, row) {// ── Read ───────────────────────────────────────────────────────++/**++ * GET /mine — the caller's own agent streams (GET / is S2's public list), newest first with the live one at the front, at most 20, and `?live=1` for++ * only the live one. This is the lookup Codes, Actor and the Dock make to find out which stream is the person's ("the++ * signed-in person's own live agent stream", Agent Streams S1: the embed is minted server-side with their token). It is++ * never a directory: no session carries nobody's rows, and another person's stream is never in the answer.++ */++router.get('/mine', auth.viewer, async (req, res) => {++ try {++ res.set('Cache-Control', 'no-store');++ if (!req.viewer) return res.json({ streams: [] });++ const onlyLive = String(req.query.live || '') === '1';++ const rows = (await service.listByUser(req.viewer.id))++ .filter((row) => !onlyLive || row.state === 'live')++ .sort((a, b) => ((b.state === 'live') - (a.state === 'live')))++ .slice(0, 20);++ res.json({ streams: rows.map(service.publicView) });++ } catch (err) {++ console.error('[AgentStream] list failed:', err.message);++ fail(res, 500, 'list_failed', 'The agent streams could not be read');++ }++});++router.get('/:id', auth.viewer, async (req, res) => {try {const row = await load(req, res);@@@ -157,6 +157,33 @@ async function check(name, fn) {const anon = await call('GET', `/api/streams/agent/${agent.public_id}`, { as: 0 });assert.strictEqual(anon.status, 404);});++ await check('only the person\'s own streams are listed, the live one first, and no ticket comes with them', async () => {++ // A second stream of ann's, live, so the list has to put it first and the filter has something to keep.++ const second = await service.create({++ user: ann, title: 'Second desk', task: { source: 'desk', ref: 'desk_2', title: 'Second' }, visibility: 'private',++ });++ await raw.prepare("UPDATE agent_streams SET state = 'live', started_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);++++ const mine = await call('GET', '/api/streams/agent/mine', { as: 1 });++ assert.strictEqual(mine.status, 200, JSON.stringify(mine.data));++ assert.deepStrictEqual(mine.data.streams.map((s) => s.id), [second.public_id, agent.public_id], 'live first, then the newest');++ for (const s of mine.data.streams) {++ assert.match(s.id, /^ags_/);++ assert.deepStrictEqual(serializers.findSecrets(s), [], 'nothing secret in a listed stream');++ }++ assert.ok(!JSON.stringify(mine.data).includes('wvt_'), 'the list mints no view ticket');++ assert.ok(!JSON.stringify(mine.data).includes('wpt_'), 'and no publish ticket');++++ const live = await call('GET', '/api/streams/agent/mine?live=1', { as: 1 });++ assert.deepStrictEqual(live.data.streams.map((s) => s.id), [second.public_id], '?live=1 keeps the live one only');++++ const theirs = await call('GET', '/api/streams/agent/mine', { as: 2 });++ assert.deepStrictEqual(theirs.data.streams, [], 'nobody else\'s streams, not even an empty-looking one of theirs');++ const anon = await call('GET', '/api/streams/agent/mine', { as: 0 });++ assert.deepStrictEqual(anon.data.streams, [], 'no session, nothing');++ // Back to ended: the rest of the file is about one stream, and a live one would take ann's slot.++ await raw.prepare("UPDATE agent_streams SET state = 'ended', ended_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);++ });await check('a person the stream is shared with can read it and gets a view ticket', async () => {assert.ok(sharing.available(), 'the pinned openvibe-sdk has sharing');// Through the public surface the Share sheet uses: the owner makes it people-only and adds bob.
@@ -3,6 +3,7 @@
@@ -3,6 +3,7 @@
* /api/streams/agent — the agent streams API (live.agent-stream@1 and the publish/view routes around it).
* /api/streams/agent — the agent streams API (live.agent-stream@1 and the publish/view routes around it).
*
*
* POST / create the stream and its publish ticket { stream, publish } 201
* POST / create the stream and its publish ticket { stream, publish } 201
+ * GET / the caller's own streams, the live one first { streams } (Dock K-D3)
* GET /:id the stream (view rule), plus what its page needs
* GET /:id the stream (view rule), plus what its page needs
* PUT /:id/visibility the owner's switch
* PUT /:id/visibility the owner's switch
* POST /:id/rotate-link a fresh unlisted link secret (the old links stop working)
* POST /:id/rotate-link a fresh unlisted link secret (the old links stop working)
@@ -161,6 +162,28 @@ async function watchable(req, row) {
@@ -161,6 +162,28 @@ async function watchable(req, row) {
// ── Read ───────────────────────────────────────────────────────
// ── Read ───────────────────────────────────────────────────────
+/**
+ * GET /mine — the caller's own agent streams (GET / is S2's public list), newest first with the live one at the front, at most 20, and `?live=1` for
+ * only the live one. This is the lookup Codes, Actor and the Dock make to find out which stream is the person's ("the
+ * signed-in person's own live agent stream", Agent Streams S1: the embed is minted server-side with their token). It is
+ * never a directory: no session carries nobody's rows, and another person's stream is never in the answer.
+ */
+router.get('/mine', auth.viewer, async (req, res) => {
+ try {
+ res.set('Cache-Control', 'no-store');
+ if (!req.viewer) return res.json({ streams: [] });
+ const onlyLive = String(req.query.live || '') === '1';
+ const rows = (await service.listByUser(req.viewer.id))
+ .filter((row) => !onlyLive || row.state === 'live')
+ .sort((a, b) => ((b.state === 'live') - (a.state === 'live')))
+ .slice(0, 20);
+ res.json({ streams: rows.map(service.publicView) });
+ } catch (err) {
+ console.error('[AgentStream] list failed:', err.message);
+ fail(res, 500, 'list_failed', 'The agent streams could not be read');
+ }
+});
+
router.get('/:id', auth.viewer, async (req, res) => {
router.get('/:id', auth.viewer, async (req, res) => {
try {
try {
const row = await load(req, res);
const row = await load(req, res);
@@ -157,6 +157,33 @@ async function check(name, fn) {
@@ -157,6 +157,33 @@ async function check(name, fn) {
const anon = await call('GET', `/api/streams/agent/${agent.public_id}`, { as: 0 });
const anon = await call('GET', `/api/streams/agent/${agent.public_id}`, { as: 0 });
assert.strictEqual(anon.status, 404);
assert.strictEqual(anon.status, 404);
});
});
+ await check('only the person\'s own streams are listed, the live one first, and no ticket comes with them', async () => {
+ // A second stream of ann's, live, so the list has to put it first and the filter has something to keep.
+ const second = await service.create({
+ user: ann, title: 'Second desk', task: { source: 'desk', ref: 'desk_2', title: 'Second' }, visibility: 'private',
+ });
+ await raw.prepare("UPDATE agent_streams SET state = 'live', started_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);
+
+ const mine = await call('GET', '/api/streams/agent/mine', { as: 1 });
+ assert.strictEqual(mine.status, 200, JSON.stringify(mine.data));
+ assert.deepStrictEqual(mine.data.streams.map((s) => s.id), [second.public_id, agent.public_id], 'live first, then the newest');
+ for (const s of mine.data.streams) {
+ assert.match(s.id, /^ags_/);
+ assert.deepStrictEqual(serializers.findSecrets(s), [], 'nothing secret in a listed stream');
+ }
+ assert.ok(!JSON.stringify(mine.data).includes('wvt_'), 'the list mints no view ticket');
+ assert.ok(!JSON.stringify(mine.data).includes('wpt_'), 'and no publish ticket');
+
+ const live = await call('GET', '/api/streams/agent/mine?live=1', { as: 1 });
+ assert.deepStrictEqual(live.data.streams.map((s) => s.id), [second.public_id], '?live=1 keeps the live one only');
+
+ const theirs = await call('GET', '/api/streams/agent/mine', { as: 2 });
+ assert.deepStrictEqual(theirs.data.streams, [], 'nobody else\'s streams, not even an empty-looking one of theirs');
+ const anon = await call('GET', '/api/streams/agent/mine', { as: 0 });
+ assert.deepStrictEqual(anon.data.streams, [], 'no session, nothing');
+ // Back to ended: the rest of the file is about one stream, and a live one would take ann's slot.
+ await raw.prepare("UPDATE agent_streams SET state = 'ended', ended_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);
+ });
await check('a person the stream is shared with can read it and gets a view ticket', async () => {
await check('a person the stream is shared with can read it and gets a view ticket', async () => {
assert.ok(sharing.available(), 'the pinned openvibe-sdk has sharing');
assert.ok(sharing.available(), 'the pinned openvibe-sdk has sharing');
// Through the public surface the Share sheet uses: the owner makes it people-only and adds bob.
// Through the public surface the Share sheet uses: the owner makes it people-only and adds bob.