"use strict"; const k_posts_max = 20; const k_votes_max = 20; var g_ready = false; var g_selected = null; var g_following_cache = {}; var g_followers_cache = {}; var g_following_deep_cache = {}; async function following(db, id) { if (g_following_cache[id]) { return g_following_cache[id]; } var o = await db.get(id + ":following"); const k_version = 5; var f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {users: [], sequence: 0, version: k_version}; } f.users = new Set(f.users); await ssb.sqlStream( "SELECT "+ " sequence, "+ " json_extract(content, '$.contact') AS contact, "+ " json_extract(content, '$.following') AS following "+ "FROM messages "+ "WHERE "+ " author = ?1 AND "+ " sequence > ?2 AND "+ " json_extract(content, '$.type') = 'contact' "+ "UNION SELECT MAX(sequence) AS sequence, NULL, NULL FROM messages WHERE author = ?1 "+ "ORDER BY sequence", [id, f.sequence], function(row) { if (row.following) { f.users.add(row.contact); } else { f.users.delete(row.contact); } f.sequence = row.sequence; }); f.users = Array.from(f.users); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":following", j); } g_following_cache[id] = f.users; return f.users; } async function followingDeep(db, seed_ids, depth) { if (depth <= 0) { return seed_ids; } var key = JSON.stringify([seed_ids, depth]); if (g_following_deep_cache[key]) { return g_following_deep_cache[key]; } var f = await Promise.all(seed_ids.map(x => following(db, x))); var ids = [].concat(...f); var x = await followingDeep(db, [...new Set(ids)].sort(), depth - 1); x = [...new Set([].concat(...x, ...seed_ids))].sort(); g_following_deep_cache[key] = x; return x; } async function followers(db, id) { if (g_followers_cache[id]) { return g_followers_cache[id]; } var results = []; var me = await ssb.whoami(); var visible_users = await followingDeep(db, [me], 2); for (let user of visible_users) { var followed = await following(db, user); if (followed.indexOf(id)) { results.push(user); } } g_followers_cache[id] = results; return results; } async function getAbout(db, id) { var o = await db.get(id + ":about"); const k_version = 4; var f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {about: {}, sequence: 0, version: k_version}; } await ssb.sqlStream( "SELECT "+ " sequence, "+ " content "+ "FROM messages "+ "WHERE "+ " sequence > ?1 AND "+ " author = ?2 AND "+ " json_extract(content, '$.type') = 'about' AND "+ " json_extract(content, '$.about') = author "+ "UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?2 "+ "ORDER BY sequence", [f.sequence, id], function(row) { f.sequence = row.sequence; if (row.content) { var about = {}; try { about = JSON.parse(row.content); } catch { } delete about.about; delete about.type; f.about = Object.assign(f.about, about); } }); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":about", j); } return f.about; } function fnv32a(value) { var result = 0x811c9dc5; for (var i = 0; i < value.length; i++) { result ^= value.charCodeAt(i); result += (result << 1) + (result << 4) + (result << 7) + (result << 8) + (result << 24); } return result >>> 0; } async function getRecentPostIds(db, id, ids, limit) { const k_version = 11; const k_batch_max = 16; var o = await db.get(id + ':recent_posts'); var recent = []; var f = o ? JSON.parse(o) : o; var ids_hash = fnv32a(JSON.stringify(ids)); if (!f || f.version != k_version || f.ids_hash != ids_hash) { f = {recent: [], rowid: 0, version: k_version, ids_hash: ids_hash}; } var row_id_max = 0; await ssb.sqlStream( "SELECT MAX(rowid) as rowid FROM messages", [], function(row) { row_id_max = row.rowid; }); for (var i = 0; i < ids.length; i += k_batch_max) { var ids_batch = ids.slice(i, Math.min(i + k_batch_max, ids.length)); await ssb.sqlStream( "SELECT "+ " rowid, "+ " id, "+ " timestamp "+ "FROM messages "+ "WHERE "+ " rowid > ? AND "+ " rowid <= ? AND "+ " author IN (" + ids_batch.map(x => '?').join(", ") + ") AND "+ " json_extract(content, '$.type') = 'post' "+ "ORDER BY timestamp DESC LIMIT ?", [].concat([f.rowid, row_id_max], ids_batch, [limit]), function(row) { if (row.id) { recent.push({id: row.id, timestamp: row.timestamp}); } }); } f.rowid = row_id_max; f.recent = [].concat(recent, f.recent); var have = {}; f.recent = f.recent.filter(function(x) { if (!have[x.id]) { have[x.id] = true; return true; } }); f.recent.sort((x, y) => y.timestamp - x.timestamp); f.recent = f.recent.slice(0, limit); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":recent_posts", j); } return f.recent.map(x => x.id); } async function getVotes(db, id) { var o = await db.get(id + ":votes"); const k_version = 5; var votes = []; var f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {votes: [], rowid: 0, version: k_version}; } await ssb.sqlStream( "SELECT "+ " rowid, "+ " author, "+ " id, "+ " sequence, "+ " timestamp, "+ " content "+ "FROM messages "+ "WHERE "+ " rowid > ? AND "+ " author = ? AND "+ " json_extract(content, '$.type') = 'vote' "+ "UNION SELECT MAX(rowid) as rowid, NULL, NULL AS id, NULL, NULL, NULL FROM messages "+ "ORDER BY rowid DESC LIMIT ?", [f.rowid, id, k_votes_max], function(row) { if (row.id) { votes.push(row); } else { f.rowid = row.rowid; } }); f.votes = [].concat(votes.reverse(), f.votes).slice(0, k_votes_max); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":votes", j); } return f.votes; } async function getPosts(db, ids) { var posts = []; if (ids.length) { await ssb.sqlStream( "SELECT rowid, * FROM messages WHERE id IN (" + ids.map(x => "?").join(", ") + ") ORDER BY timestamp DESC", ids, row => posts.push(row)); } return posts; } async function ready() { g_ready = true; return refresh(g_selected); } ssb.addEventListener('broadcasts', async function() { await app.postMessage({broadcasts: await ssb.getBroadcasts()}); }); core.register('onConnectionsChanged', async function() { var connections = await ssb.connections(); await app.postMessage({connections: connections}); }); async function refresh(selected) { g_following_cache = {}; g_followers_cache = {}; g_following_deep_cache = {}; await app.postMessage({clear: true}); var whoami = await ssb.whoami(); var db = await database("ssb"); var all_followed = await followingDeep(db, [whoami], 2); if (selected) { g_selected = selected; } else { g_selected = all_followed; } await Promise.all([ app.postMessage({whoami: whoami}), app.postMessage({hash: selected && selected.length == 1 ? selected[0] : null}), ssb.getBroadcasts().then(broadcasts => app.postMessage({broadcasts: broadcasts})), ssb.connections().then(connections => app.postMessage({connections: connections})), core.apps().then(apps => app.postMessage({apps: apps})), getRecentPostIds(db, whoami, g_selected, k_posts_max).then(async function(ids) { var posts = await getPosts(db, ids); var roots = posts.map(function(x) { try { return JSON.parse(x.content).root; } catch { return null; } }); var have = new Set(posts.map(x => x.id)); roots = [...new Set(roots)].filter(x => x && !have.has(x)); var all_posts = [].concat(posts, await getPosts(db, roots)); return Promise.all(all_posts.map(x => app.postMessage({message: x}))); }), Promise.all([].concat(all_followed.map(function(id) { return [ getVotes(db, id).then(votes => app.postMessage({votes: votes})), getAbout(db, id).then(user => app.postMessage({user: {user: id, about: user}})), following(db, id).then(f => app.postMessage({following: {id: id, users: f}})), followers(db, id).then(f => app.postMessage({followers: {id: id, users: f}})), ]; }))), ]); await app.postMessage({ready: true}); } ssb.addEventListener('message', async function(id) { var db = await database("ssb"); var posts = await getPosts(db, [id]); for (let post of posts) { if (post.author == await ssb.whoami()) { await app.postMessage({message: post}); } else { await app.postMessage({unread: 1}); } } }); core.register('message', async function(m) { if (m.message == 'ready') { await ready(); } else if (m.message) { if (m.message.connect) { await ssb.connect(m.message.connect); } else if (m.message.appendMessage) { await ssb.appendMessage(m.message.appendMessage); } else if (m.message.refresh) { await refresh(g_selected); } } else if (m.event == 'hashChange') { if (m.hash.length > 1) { g_selected = [m.hash.substring(1)]; } else { g_selected = null; } if (g_ready) { await refresh(g_selected); } } else if (m.event == 'focus' || m.event == 'blur') { /* Shh. */ } else { print(JSON.stringify(m)); } }); async function main() { if (core.user && core.user.credentials && core.user.credentials.permissions && core.user.credentials.permissions.administration) { await app.setDocument(utf8Decode(await getFile("index.html"))); } else { await app.setDocument('
Only the administrator can use this app at this time. Login at the top right.
'); } } main();