"use strict"; const k_posts_max = 20; const k_votes_max = 20; 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 = [].concat(...x, ...seed_ids); 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); 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() { return refresh(); } 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() { 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"); await Promise.all([ app.postMessage({whoami: whoami}), app.postMessage({broadcasts: await ssb.getBroadcasts()}), app.postMessage({connections: await ssb.connections()}), app.postMessage({apps: await core.apps()}), followingDeep(db, [whoami], 2).then(function(f) { return getRecentPostIds(db, whoami, [].concat([whoami], f), k_posts_max).then(async function(ids) { return getPosts(db, ids); }).then(async function(posts) { var roots = posts.map(function(x) { try { return JSON.parse(x.content).root; } catch { return null; } }); roots = roots.filter(function(root) { return root && posts.every(post => post.id != root); }); return [].concat(posts, await getPosts(db, roots)); }).then(function(posts) { return Promise.all(posts.map(x => app.postMessage({message: x}))); }).then(function() { return Promise.all(f.map(function(id) { return Promise.all([ getVotes(db, id).then(function(votes) { return app.postMessage({votes: votes}); }), getAbout(db, id).then(function(user) { return app.postMessage({user: {user: id, about: user}}); }), following(db, id).then(function(following) { return app.postMessage({following: {id: id, users: following}}); }), followers(db, id).then(function(followers) { return app.postMessage({followers: {id: id, users: followers}}); }), ]); })); }); }), ]); } 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(); } } 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();