let g_about_cache = {}; async function query(sql, args) { let result = []; await ssb.sqlAsync(sql, args, function(row) { result.push(row); }); return result; } async function contacts_internal(id, last_row_id, following, max_row_id) { let result = Object.assign({}, following[id] || {}); result.following = result.following || {}; result.blocking = result.blocking || {}; let contacts = await query( ` SELECT content FROM messages WHERE author = ? AND rowid > ? AND rowid <= ? AND json_extract(content, '$.type') = 'contact' ORDER BY sequence `, [id, last_row_id, max_row_id]); for (let row of contacts) { let contact = JSON.parse(row.content); if (contact.following === true) { result.following[contact.contact] = true; } else if (contact.following === false) { delete result.following[contact.contact]; } else if (contact.blocking === true) { result.blocking[contact.contact] = true; } else if (contact.blocking === false) { delete result.blocking[contact.contact]; } } following[id] = result; return result; } async function contact(id, last_row_id, following, max_row_id) { return await contacts_internal(id, last_row_id, following, max_row_id); } async function following_deep_internal(ids, depth, blocking, last_row_id, following, max_row_id) { let contacts = await Promise.all([...new Set(ids)].map(x => contact(x, last_row_id, following, max_row_id))); let result = {}; for (let i = 0; i < ids.length; i++) { let id = ids[i]; let contact = contacts[i]; let all_blocking = Object.assign({}, contact.blocking, blocking); let found = Object.keys(contact.following).filter(y => !all_blocking[y]); let deeper = depth > 1 ? await following_deep_internal(found, depth - 1, all_blocking, last_row_id, following, max_row_id) : []; result[id] = [id, ...found, ...deeper]; } return [...new Set(Object.values(result).flat())]; } async function following_deep(ids, depth, blocking) { let db = await database('cache'); const k_cache_version = 5; let cache = await db.get('following'); cache = cache ? JSON.parse(cache) : {}; if (cache.version !== k_cache_version) { cache = { version: k_cache_version, following: {}, last_row_id: 0, }; } let max_row_id = (await query(` SELECT MAX(rowid) AS max_row_id FROM messages `, []))[0].max_row_id; let result = await following_deep_internal(ids, depth, blocking, cache.last_row_id, cache.following, max_row_id); cache.last_row_id = max_row_id; let store = JSON.stringify(cache); await db.set('following', store); return result; } async function fetch_about(db, ids, users) { const k_cache_version = 1; let cache = await db.get('about'); cache = cache ? JSON.parse(cache) : {}; if (cache.version !== k_cache_version) { cache = { version: k_cache_version, about: {}, last_row_id: 0, }; } let max_row_id = 0; await ssb.sqlAsync(` SELECT MAX(rowid) AS max_row_id FROM messages `, [], function(row) { max_row_id = row.max_row_id; }); for (let id of Object.keys(cache.about)) { if (ids.indexOf(id) == -1) { delete cache.about[id]; } } let abouts = []; await ssb.sqlAsync( ` SELECT messages.* FROM messages, json_each(?1) AS following WHERE messages.author = following.value AND messages.rowid > ?3 AND messages.rowid <= ?4 AND json_extract(messages.content, '$.type') = 'about' UNION SELECT messages.* FROM messages, json_each(?2) AS following WHERE messages.author = following.value AND messages.rowid <= ?4 AND json_extract(messages.content, '$.type') = 'about' ORDER BY messages.author, messages.sequence `, [ JSON.stringify(ids.filter(id => cache.about[id])), JSON.stringify(ids.filter(id => !cache.about[id])), cache.last_row_id, max_row_id, ]); for (let about of abouts) { let content = JSON.parse(about.content); if (content.about === about.author) { delete content.type; delete content.about; cache.about[about.author] = Object.assign(cache.about[about.author] || {}, content); } } cache.last_row_id = max_row_id; await db.set('about', JSON.stringify(cache)); users = users || {}; for (let id of Object.keys(cache.about)) { users[id] = Object.assign(users[id] || {}, cache.about[id]); } return Object.assign({}, users); } async function getAbout(db, id) { if (g_about_cache[id]) { return g_about_cache[id]; } let o = await db.get(id + ":about"); const k_version = 4; let f = o ? JSON.parse(o) : o; if (!f || f.version != k_version) { f = {about: {}, sequence: 0, version: k_version}; } await ssb.sqlAsync( "SELECT "+ " sequence, "+ " content "+ "FROM messages "+ "WHERE "+ " author = ?1 AND "+ " sequence > ?2 AND "+ " json_extract(content, '$.type') = 'about' AND "+ " json_extract(content, '$.about') = ?1 "+ "UNION SELECT MAX(sequence) as sequence, NULL FROM messages WHERE author = ?1 "+ "ORDER BY sequence", [id, f.sequence], function(row) { f.sequence = row.sequence; if (row.content) { let about = {}; try { about = JSON.parse(row.content); } catch { } delete about.about; delete about.type; f.about = Object.assign(f.about, about); } }); let j = JSON.stringify(f); if (o != j) { await db.set(id + ":about", j); } g_about_cache[id] = f.about; return f.about; } async function getSize(db, id) { let size = 0; await ssb.sqlAsync( "SELECT (LENGTH(author) * COUNT(*) + SUM(LENGTH(content)) + SUM(LENGTH(id))) AS size FROM messages WHERE author = ?1", [id], function (row) { size += row.size; }); return size; } async function getSizes(ids) { let sizes = {}; await ssb.sqlAsync( ` SELECT author, (SUM(LENGTH(content)) + SUM(LENGTH(author)) + SUM(LENGTH(messages.id))) AS size FROM messages JOIN json_each(?) AS ids ON author = ids.value GROUP BY author `, [JSON.stringify(ids)], function (row) { sizes[row.author] = row.size; }); return sizes; } function niceSize(bytes) { let value = bytes; let unit = 'B'; const k_units = ['kB', 'MB', 'GB', 'TB']; for (let u of k_units) { if (value >= 1024) { value /= 1024; unit = u; } else { break; } } return Math.round(value * 10) / 10 + ' ' + unit; } function escape(value) { return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); } async function main() { await app.setDocument('
building...'); let db = await database('ssb'); let whoami = await ssb.getIdentities(); let tree = ''; await app.setDocument(`
Enumerating followed users...`); let following = await following_deep(whoami, 2, {}); await app.setDocument(`
Getting names and sizes...`); let [about, sizes] = await Promise.all([ fetch_about(db, following, {}), getSizes(following), ]); await app.setDocument(`
Finishing...`); following.sort((a, b) => ((sizes[b] ?? 0) - (sizes[a] ?? 0))); for (let id of following) { tree += `