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 += `
  • ${escape(about[id]?.name ?? id)} ${niceSize(sizes[id] ?? 0)}
  • \n`; } await app.setDocument( '\n\n

    Following

    \n\n\n' ); } main();