openvibers/openvibe.shared history.js

history.js

main 5.7 KB · 115 lines · javascript

1/**
2 * openvibe-shared/history.js — cross-site history for a signed-in account.
3 *
4 * OpenVibeHistory.record({ type, title, url, icon, service, meta }, { token, apiBase })
5 * OpenVibeHistory.recent({ limit, service, type, token, apiBase }) → [{...}]
6 * OpenVibeHistory.clear({ token }) → wipes it
7 *
8 * Every property records what the account touched — a tool used, a stream watched, a paste
9 * opened, a game played — so "Recently used" in the shared navbar and the History tab on the
10 * Network show one timeline across the whole network instead of one per site. Writes are
11 * deduplicated server-side (same URL within ten minutes bumps the entry instead of adding one)
12 * and the browser side throttles too, so a page cannot flood it. Nothing is recorded for
13 * anonymous sessions, and the user can pause or clear the timeline from their account page.
14 *
15 * Loaded lazily by navbar.js from https://openvibe.network/shared/history.js; safe to include
16 * directly. Token resolution matches the navbar: ov_token cookie → localStorage.ov_token.
17 */
18(function (root) {
19 'use strict';
20 if (root.OpenVibeHistory) return;
21
22 const DEFAULT_API = 'https://openvibe.network';
23 const SENT_KEY = 'ov_history_sent'; // sessionStorage: url → ts, to avoid re-posting on every re-render
24 const THROTTLE_MS = 5 * 60 * 1000;
25
26 function token(opts) {
27 if (opts && opts.token) return opts.token;
28 const m = (typeof document !== 'undefined' ? document.cookie : '').match(/(?:^|;\s*)ov_token=([^;]*)/);
29 if (m && m[1]) { try { return decodeURIComponent(m[1]); } catch { return m[1]; } }
30 try { return localStorage.getItem('ov_token'); } catch { return null; }
31 }
32
33 function api(opts) { return (opts && opts.apiBase) || DEFAULT_API; }
34
35 function sent() { try { return JSON.parse(sessionStorage.getItem(SENT_KEY) || '{}'); } catch { return {}; } }
36 function markSent(url) {
37 try {
38 const m = sent(); m[url] = Date.now();
39 const keys = Object.keys(m); if (keys.length > 60) for (const k of keys.slice(0, keys.length - 60)) delete m[k];
40 sessionStorage.setItem(SENT_KEY, JSON.stringify(m));
41 } catch { /* storage unavailable */ }
42 }
43
44 /** Guess an icon and service label from the URL when the caller gives none. */
45 function inferService(url) {
46 let host = '';
47 try { host = new URL(url, location.href).hostname; } catch { return { service: null }; }
48 const m = host.match(/^(?:(.+)\.)?openvibe\.([a-z]+)$/) || host.match(/^(?:(.+)\.)?(openre)\.stream$/);
49 if (!m) return { service: null };
50 return { service: m[2], sub: m[1] || null };
51 }
52
53 const ICONS = { tool: 'fa-screwdriver-wrench', stream: 'fa-tower-broadcast', vod: 'fa-clapperboard', clip: 'fa-scissors', paste: 'fa-paste', game: 'fa-gamepad', page: 'fa-file-lines', post: 'fa-comments', account: 'fa-user', theme: 'fa-palette', download: 'fa-download', chat: 'fa-comment' };
54
55 async function record(entry, opts) {
56 const t = token(opts);
57 if (!t || !entry) return false;
58 const url = String(entry.url || (typeof location !== 'undefined' ? location.href : '')).slice(0, 2000);
59 if (!url) return false;
60 const last = sent()[url] || 0;
61 if (!entry.force && Date.now() - last < THROTTLE_MS) return false;
62 markSent(url);
63 const inf = inferService(url);
64 const body = {
65 type: String(entry.type || 'page').slice(0, 32),
66 title: String(entry.title || (typeof document !== 'undefined' ? document.title : '') || url).slice(0, 200),
67 url,
68 image_url: String(entry.image_url || entry.picture || '').slice(0, 2000) || null,
69 private: entry.private === true,
70 mature: entry.mature === true,
71 icon: entry.icon || ICONS[entry.type] || null,
72 service: entry.service || inf.service || null,
73 sub: entry.sub || inf.sub || null,
74 meta: entry.meta && typeof entry.meta === 'object' ? entry.meta : null,
75 };
76 try {
77 const res = await fetch(`${api(opts)}/api/history`, {
78 method: 'POST', keepalive: true,
79 headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${t}` },
80 body: JSON.stringify(body),
81 });
82 return res.ok;
83 } catch { return false; }
84 }
85
86 async function recent(opts) {
87 const t = token(opts);
88 if (!t) return [];
89 const q = new URLSearchParams();
90 if (opts && opts.limit) q.set('limit', String(opts.limit));
91 if (opts && opts.service) q.set('service', opts.service);
92 if (opts && opts.type) q.set('type', opts.type);
93 try {
94 const res = await fetch(`${api(opts)}/api/history?${q}`, { headers: { Authorization: `Bearer ${t}` } });
95 if (!res.ok) return [];
96 const data = await res.json();
97 return (data && data.items) || [];
98 } catch { return []; }
99 }
100
101 async function clear(opts) {
102 const t = token(opts);
103 if (!t) return false;
104 try { const res = await fetch(`${api(opts)}/api/history`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } }); return res.ok; } catch { return false; }
105 }
106
107 async function remove(id, opts) {
108 const t = token(opts);
109 if (!t) return false;
110 try { const res = await fetch(`${api(opts)}/api/history/${encodeURIComponent(id)}`, { method: 'DELETE', headers: { Authorization: `Bearer ${t}` } }); return res.ok; } catch { return false; }
111 }
112
113 root.OpenVibeHistory = { record, recent, clear, remove, ICONS };
114})(typeof window !== 'undefined' ? window : globalThis);
115