Some checks failed
		
		
	
	Build Tilde Friends / Build-All (push) Has been cancelled
				
			
		
			
				
	
	
		
			274 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			274 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
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 json(content) AS 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 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('<pre style="color: #fff">building...</pre>');
 | 
						|
	let db = await database('ssb');
 | 
						|
	let whoami = await ssb.getIdentities();
 | 
						|
	let tree = '';
 | 
						|
	await app.setDocument(
 | 
						|
		`<pre style="color: #fff">Enumerating followed users...</pre>`
 | 
						|
	);
 | 
						|
	let following = await following_deep(whoami, 2, {});
 | 
						|
	await app.setDocument(
 | 
						|
		`<pre style="color: #fff">Getting names and sizes...</pre>`
 | 
						|
	);
 | 
						|
	let [about, sizes] = await Promise.all([
 | 
						|
		fetch_about(db, following, {}),
 | 
						|
		getSizes(following),
 | 
						|
	]);
 | 
						|
	await app.setDocument(`<pre style="color: #fff">Finishing...</pre>`);
 | 
						|
	following.sort((a, b) => (sizes[b] ?? 0) - (sizes[a] ?? 0));
 | 
						|
	for (let id of following) {
 | 
						|
		tree += `<li><a href="/~core/ssb/#${id}">${escape(about[id]?.name ?? id)}</a> ${niceSize(sizes[id] ?? 0)}</li>\n`;
 | 
						|
	}
 | 
						|
	await app.setDocument(
 | 
						|
		'<!DOCTYPE html>\n<html>\n<body style="color: #fff"><h1>Following</h1>\n<ul>' +
 | 
						|
			tree +
 | 
						|
			'</ul>\n</body>\n</html>'
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
main();
 |