"use strict"; const k_posts_max = 20; const k_votes_max = 20; async function following(db, 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); } 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 = 3; 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], 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 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 = 9; 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, "+ " timestamp "+ "FROM messages "+ "WHERE "+ " rowid > ? AND "+ " author IN (" + ids.map(x => '?').join(", ") + ") AND "+ " json_extract(content, '$.type') IN ('post', 'tildefriends-app') "+ "UNION SELECT MAX(rowid) as rowid, NULL, NULL FROM messages "+ "ORDER BY timestamp 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 = 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() { 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) { 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}))); }); 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}}); }), ]); })); }), sendUser(db, whoami), ]); } ssb.addEventListener('message', async function(id) { var db = await database("ssb"); var posts = await getPosts(db, [id]); for (let post of posts) { await app.postMessage({message: post}); } }); 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.share_app) { var app = await ssb.blobGet(m.message.share_app.app); app = JSON.parse(utf8Decode(app)); app.type = 'tildefriends-app'; app.name = m.message.share_app.name; app.text = m.message.share_app.text; await ssb.appendMessage(app); } else if (m.message.user) { await sendUser(await database("ssb"), m.message.user); } 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('