openvibers/openvibe.network Commits
Frame feed relay: the new kinds and the filters pass to Community (#716)
Changes
@@@ -435,14 +435,15 @@ async function createFrameService(db, config, analytics, { privateKey = null, isres.send(JSON.stringify(pulse || { live_now: null, watching: null, sites: orderedOpen().length, views_7d: null, live: null, updated: null }));});-- // GET /api/frame/feed?kind=&cursor=&limit= and GET /api/frame/wall?limit=&after=&for= — the menu's Feed and wall++ // GET /api/frame/feed?kind=&site=&q=&following=1&cursor=&limit= and GET /api/frame/wall?limit=&after=&for= — the menu's Feed and wall// (openvibe-shared ov-home.js) on every site, asked through Network because every site's connect-src lets the navbar// reach openvibe.network, and the sites' policies refused openvibe.community (11-09: the Feed fell back to its old// sections everywhere but five sites). Community's /api/v1/feed and /api/v1/pulse answer; the person's Bearer token// goes along when the menu sends one (a personal wall, the composer's can_post), never a cookie. Anonymous answers are// kept 30 s per query; personal ones are not kept. Community not answering is an empty page, never an error.const relayed = new Map();-- const FEED_KINDS = ['live', 'stream', 'clip', 'video', 'pic', 'post', 'meme', 'game', 'update'];++ // Community's feed kinds (server/pulse/feed.js KINDS; #310 added wiki, quest, chat, event and member).++ const FEED_KINDS = ['live', 'stream', 'clip', 'video', 'pic', 'post', 'meme', 'game', 'update', 'wiki', 'quest', 'chat', 'event', 'member'];async function relay(req, res, path, params) {res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization', 'Vary': 'Authorization', 'Content-Type': 'application/json; charset=utf-8' });const auth = /^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '';@@@ -483,8 +484,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, isreturn res.status(r.status).send(text.slice(0, 64 * 1024));} catch { return res.status(502).send('{"error":"Community did not answer"}'); }});++ // The feed's filters pass through: a site (a service id), words (q, at most 80 characters, one line) and the++ // signed-in person's follows (following=1 only with their Bearer token: Community answers 401 without one).++ const qSite = (v) => (/^[a-z0-9][a-z0-9:-]{0,39}$/.test(String(v || '')) ? String(v) : '');++ const qWords = (v) => String(v == null ? '' : v).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80);router.get('/feed', (req, res) => relay(req, res, '/api/v1/feed', {kind: FEED_KINDS.includes(String(req.query.kind || '')) ? String(req.query.kind) : '',++ site: qSite(req.query.site), q: qWords(req.query.q),++ following: /^(1|true)$/.test(String(req.query.following || '')) ? '1' : '',cursor: qCursor(req.query.cursor), limit: qLimit(req.query.limit, 40),}));router.get('/wall', (req, res) => relay(req, res, '/api/v1/pulse', {@@@ -52,6 +52,16 @@ const { createFrameService } = require('../server/frame/service');const odd = new URL(asked[asked.length - 1].url, 'http://x');assert.ok(!odd.searchParams.has('kind') && !odd.searchParams.has('cursor') && odd.searchParams.get('limit') === '40', 'an odd kind and cursor are dropped and the limit capped');++ // The new kinds and the filters reach Community; odd values do not.++ ({ d } = await get('/feed?kind=quest&site=wiki&q=%20head%0Aslider%20&following=1', { Authorization: 'Bearer abcdefghijklmnop.qrstu' }));++ const f = new URL(asked[asked.length - 1].url, 'http://x');++ assert.deepStrictEqual([f.searchParams.get('kind'), f.searchParams.get('site'), f.searchParams.get('q'), f.searchParams.get('following')], ['quest', 'wiki', 'head slider', '1'], 'kind, site, words and following pass');++ assert.strictEqual(asked[asked.length - 1].auth, 'Bearer abcdefghijklmnop.qrstu', 'following goes with the person');++ for (const k of ['wiki', 'chat', 'event', 'member']) { ({ d } = await get(`/feed?kind=${k}`)); assert.strictEqual(new URL(asked[asked.length - 1].url, 'http://x').searchParams.get('kind'), k, `${k} passes`); }++ ({ d } = await get('/feed?site=../x&q=' + 'a'.repeat(200) + '&following=yes'));++ const g = new URL(asked[asked.length - 1].url, 'http://x');++ assert.ok(!g.searchParams.has('site') && g.searchParams.get('q').length === 80 && !g.searchParams.has('following'), 'an odd site and following are dropped, words capped at 80');++const pre = await fetch(`${base}/feed`, { method: 'OPTIONS', headers: { Origin: 'https://openvibe.video', 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'authorization' } });assert.strictEqual(pre.status, 204);assert.match(pre.headers.get('access-control-allow-headers') || '', /Authorization/i);
@@ -435,14 +435,15 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -435,14 +435,15 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
res.send(JSON.stringify(pulse || { live_now: null, watching: null, sites: orderedOpen().length, views_7d: null, live: null, updated: null }));
res.send(JSON.stringify(pulse || { live_now: null, watching: null, sites: orderedOpen().length, views_7d: null, live: null, updated: null }));
});
});
- // GET /api/frame/feed?kind=&cursor=&limit= and GET /api/frame/wall?limit=&after=&for= — the menu's Feed and wall
+ // GET /api/frame/feed?kind=&site=&q=&following=1&cursor=&limit= and GET /api/frame/wall?limit=&after=&for= — the menu's Feed and wall
// (openvibe-shared ov-home.js) on every site, asked through Network because every site's connect-src lets the navbar
// (openvibe-shared ov-home.js) on every site, asked through Network because every site's connect-src lets the navbar
// reach openvibe.network, and the sites' policies refused openvibe.community (11-09: the Feed fell back to its old
// reach openvibe.network, and the sites' policies refused openvibe.community (11-09: the Feed fell back to its old
// sections everywhere but five sites). Community's /api/v1/feed and /api/v1/pulse answer; the person's Bearer token
// sections everywhere but five sites). Community's /api/v1/feed and /api/v1/pulse answer; the person's Bearer token
// goes along when the menu sends one (a personal wall, the composer's can_post), never a cookie. Anonymous answers are
// goes along when the menu sends one (a personal wall, the composer's can_post), never a cookie. Anonymous answers are
// kept 30 s per query; personal ones are not kept. Community not answering is an empty page, never an error.
// kept 30 s per query; personal ones are not kept. Community not answering is an empty page, never an error.
const relayed = new Map();
const relayed = new Map();
- const FEED_KINDS = ['live', 'stream', 'clip', 'video', 'pic', 'post', 'meme', 'game', 'update'];
+ // Community's feed kinds (server/pulse/feed.js KINDS; #310 added wiki, quest, chat, event and member).
+ const FEED_KINDS = ['live', 'stream', 'clip', 'video', 'pic', 'post', 'meme', 'game', 'update', 'wiki', 'quest', 'chat', 'event', 'member'];
async function relay(req, res, path, params) {
async function relay(req, res, path, params) {
res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization', 'Vary': 'Authorization', 'Content-Type': 'application/json; charset=utf-8' });
res.set({ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Authorization', 'Vary': 'Authorization', 'Content-Type': 'application/json; charset=utf-8' });
const auth = /^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '';
const auth = /^Bearer [A-Za-z0-9._~+/=-]{10,4096}$/.test(String(req.get('authorization') || '')) ? req.get('authorization') : '';
@@ -483,8 +484,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
@@ -483,8 +484,14 @@ async function createFrameService(db, config, analytics, { privateKey = null, is
return res.status(r.status).send(text.slice(0, 64 * 1024));
return res.status(r.status).send(text.slice(0, 64 * 1024));
} catch { return res.status(502).send('{"error":"Community did not answer"}'); }
} catch { return res.status(502).send('{"error":"Community did not answer"}'); }
});
});
+ // The feed's filters pass through: a site (a service id), words (q, at most 80 characters, one line) and the
+ // signed-in person's follows (following=1 only with their Bearer token: Community answers 401 without one).
+ const qSite = (v) => (/^[a-z0-9][a-z0-9:-]{0,39}$/.test(String(v || '')) ? String(v) : '');
+ const qWords = (v) => String(v == null ? '' : v).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 80);
router.get('/feed', (req, res) => relay(req, res, '/api/v1/feed', {
router.get('/feed', (req, res) => relay(req, res, '/api/v1/feed', {
kind: FEED_KINDS.includes(String(req.query.kind || '')) ? String(req.query.kind) : '',
kind: FEED_KINDS.includes(String(req.query.kind || '')) ? String(req.query.kind) : '',
+ site: qSite(req.query.site), q: qWords(req.query.q),
+ following: /^(1|true)$/.test(String(req.query.following || '')) ? '1' : '',
cursor: qCursor(req.query.cursor), limit: qLimit(req.query.limit, 40),
cursor: qCursor(req.query.cursor), limit: qLimit(req.query.limit, 40),
}));
}));
router.get('/wall', (req, res) => relay(req, res, '/api/v1/pulse', {
router.get('/wall', (req, res) => relay(req, res, '/api/v1/pulse', {
@@ -52,6 +52,16 @@ const { createFrameService } = require('../server/frame/service');
@@ -52,6 +52,16 @@ const { createFrameService } = require('../server/frame/service');
const odd = new URL(asked[asked.length - 1].url, 'http://x');
const odd = new URL(asked[asked.length - 1].url, 'http://x');
assert.ok(!odd.searchParams.has('kind') && !odd.searchParams.has('cursor') && odd.searchParams.get('limit') === '40', 'an odd kind and cursor are dropped and the limit capped');
assert.ok(!odd.searchParams.has('kind') && !odd.searchParams.has('cursor') && odd.searchParams.get('limit') === '40', 'an odd kind and cursor are dropped and the limit capped');
+ // The new kinds and the filters reach Community; odd values do not.
+ ({ d } = await get('/feed?kind=quest&site=wiki&q=%20head%0Aslider%20&following=1', { Authorization: 'Bearer abcdefghijklmnop.qrstu' }));
+ const f = new URL(asked[asked.length - 1].url, 'http://x');
+ assert.deepStrictEqual([f.searchParams.get('kind'), f.searchParams.get('site'), f.searchParams.get('q'), f.searchParams.get('following')], ['quest', 'wiki', 'head slider', '1'], 'kind, site, words and following pass');
+ assert.strictEqual(asked[asked.length - 1].auth, 'Bearer abcdefghijklmnop.qrstu', 'following goes with the person');
+ for (const k of ['wiki', 'chat', 'event', 'member']) { ({ d } = await get(`/feed?kind=${k}`)); assert.strictEqual(new URL(asked[asked.length - 1].url, 'http://x').searchParams.get('kind'), k, `${k} passes`); }
+ ({ d } = await get('/feed?site=../x&q=' + 'a'.repeat(200) + '&following=yes'));
+ const g = new URL(asked[asked.length - 1].url, 'http://x');
+ assert.ok(!g.searchParams.has('site') && g.searchParams.get('q').length === 80 && !g.searchParams.has('following'), 'an odd site and following are dropped, words capped at 80');
+
const pre = await fetch(`${base}/feed`, { method: 'OPTIONS', headers: { Origin: 'https://openvibe.video', 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'authorization' } });
const pre = await fetch(`${base}/feed`, { method: 'OPTIONS', headers: { Origin: 'https://openvibe.video', 'Access-Control-Request-Method': 'GET', 'Access-Control-Request-Headers': 'authorization' } });
assert.strictEqual(pre.status, 204);
assert.strictEqual(pre.status, 204);
assert.match(pre.headers.get('access-control-allow-headers') || '', /Authorization/i);
assert.match(pre.headers.get('access-control-allow-headers') || '', /Authorization/i);