"use strict"; const k_posts_max = 20; const k_votes_max = 100; async function following(db, id) { var o = await db.get(id + ":following"); const k_version = 4; 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], async 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); } return f.users; } async function followingDeep(db, seed_ids, depth) { if (depth <= 0) { return seed_ids; } 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); return x; } async function followers(db, id) { var o = await db.get(id + ":followers"); const k_version = 2; var f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {users: [], rowid: 0, version: k_version}; } f.users = new Set(f.users); await ssb.sqlStream( "SELECT "+ " rowid, "+ " author AS contact, "+ " json_extract(content, '$.following') AS following "+ "FROM messages "+ "WHERE "+ " rowid > $1 AND "+ " json_extract(content, '$.type') = 'contact' AND "+ " json_extract(content, '$.contact') = $2 "+ "UNION SELECT MAX(rowid) as rowid, NULL, NULL FROM messages "+ "ORDER BY rowid", [f.rowid, id], async function(row) { if (row.following) { f.users.add(row.contact); } else { f.users.delete(row.contact); } f.rowid = row.rowid; }); f.users = Array.from(f.users); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":followers", j); } return f.users; } async function sendUser(db, id) { return Promise.all([ following(db, id).then(async function(following) { return app.postMessage({following: {id: id, users: following}}); }), followers(db, id).then(async function(followers) { return app.postMessage({followers: {id: id, users: followers}}); }), ]); } async function pubsByUser(db, id) { var o = await db.get(id + ":pubs"); const k_version = 2; var f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {pubs: [], sequence: 0, version: k_version}; } f.pubs = Object.fromEntries(f.pubs.map(x => [JSON.stringify(x), x])); await ssb.sqlStream( "SELECT "+ " sequence, "+ " json_extract(content, '$.address.host') AS host, "+ " json_extract(content, '$.address.port') AS port, "+ " json_extract(content, '$.address.key') AS key "+ "FROM messages "+ "WHERE "+ " sequence > ?1 AND "+ " author = ?2 AND "+ " json_extract(content, '$.type') = 'pub' "+ "UNION SELECT MAX(sequence) as sequence, NULL, NULL, NULL FROM messages WHERE author = ?2 "+ "ORDER BY sequence", [f.sequence, id], async function(row) { f.sequence = row.sequence; if (row.host) { row = {host: row.host, port: row.port, key: row.key}; f.pubs[JSON.stringify(row)] = row; } }); f.pubs = Object.values(f.pubs); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":pubs", j); } return f.pubs; } async function visiblePubs(db, id) { var ids = [id].concat(await following(db, id)); var pubs = {}; for (var follow of ids) { var followPubs = await pubsByUser(db, follow); for (var pub of followPubs) { pubs[JSON.stringify(pub)] = pub; } } return Object.values(pubs); } 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], async 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 = 6; 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}; } await ssb.sqlStream( "SELECT "+ " rowid, "+ " id "+ "FROM messages "+ "WHERE "+ " rowid > ? AND "+ " author IN (" + ids.map(x => '?').join(", ") + ") AND "+ " json_extract(content, '$.type') = 'post' "+ "UNION SELECT MAX(rowid) as rowid, NULL FROM messages "+ "ORDER BY rowid DESC LIMIT ?", [].concat([f.rowid], ids, [limit + 1]), function(row) { if (row.id) { recent.push(row.id); } if (row.rowid) { f.rowid = row.rowid; } }); f.recent = [].concat(recent, f.recent).slice(0, limit); var j = JSON.stringify(f); if (o != j) { await db.set(id + ":recent_posts", j); } return f.recent; } async function getVotes(db, id) { var o = await db.get(id + ":votes"); const k_version = 2; 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], async 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(", ") + ")", ids, async function(row) { try { posts.push(row); } catch { } }); } return posts; } async function ready() { return refresh(); } core.register('onBroadcastsChanged', 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() { await app.postMessage({clear: true}); var whoami = await ssb.whoami(); var db = await database("ssb"); await Promise.all([ app.postMessage({whoami: whoami}), app.postMessage({pubs: await visiblePubs(db, whoami)}), app.postMessage({broadcasts: await ssb.getBroadcasts()}), app.postMessage({connections: await ssb.connections()}), followingDeep(db, [whoami], 2).then(function(f) { 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(async function(posts) { posts.forEach(async function(post) { await app.postMessage({message: post}); }); }); f.forEach(async function(id) { await Promise.all([ getVotes(db, id).then(async function(votes) { return Promise.all(votes.map(vote => app.postMessage({vote: vote}))); }), getAbout(db, id).then(async function(user) { return app.postMessage({user: {user: id, about: user}}); }), ]); }); }), sendUser(db, whoami), ]); } 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.post) { await ssb.post(m.message.post); } else if (m.message.appendMessage) { await ssb.appendMessage(m.message.appendMessage); } else if (m.message.user) { await sendUser(await database("ssb"), m.message.user); } else if (m.message.refresh) { await refresh(); } } else { print(JSON.stringify(m)); } }); async function main() { await app.setDocument(utf8Decode(await getFile("index.html"))); } main();