apps: Add a very wip web app.
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build Tilde Friends / Build-All (push) Successful in 30m8s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build Tilde Friends / Build-All (push) Successful in 30m8s
				
			This commit is contained in:
		
							
								
								
									
										5
									
								
								apps/web.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								apps/web.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
	"type": "tildefriends-app",
 | 
			
		||||
	"emoji": "🕸",
 | 
			
		||||
	"previous": "&n7hu5b8/TsfiG6FDlCRG5nPCrIdCr96+xpIJ/aQT/uM=.sha256"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								apps/web/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								apps/web/app.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
let g_hash;
 | 
			
		||||
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function(row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function resolve(id) {
 | 
			
		||||
	try {
 | 
			
		||||
		let blob = await ssb.blobGet(id);
 | 
			
		||||
		if (blob) {
 | 
			
		||||
			let json;
 | 
			
		||||
			try {
 | 
			
		||||
				json = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			} catch {
 | 
			
		||||
				return {id: utf8Decode(blob)};
 | 
			
		||||
			}
 | 
			
		||||
			if (json?.links) {
 | 
			
		||||
				for (let [key, value] of Object.entries(json.links)) {
 | 
			
		||||
					json.links[key] = await resolve(value);
 | 
			
		||||
				}
 | 
			
		||||
				return json;
 | 
			
		||||
			} else {
 | 
			
		||||
				return 'huh?' + json;
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return `missing<${id}>`;
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		return id + ': ' + e.message;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function get_names(identities) {
 | 
			
		||||
	return Object.fromEntries((await query(`
 | 
			
		||||
		SELECT author, name FROM (
 | 
			
		||||
			SELECT
 | 
			
		||||
				messages.author,
 | 
			
		||||
				RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
 | 
			
		||||
				messages.content ->> 'name' AS name
 | 
			
		||||
			FROM messages
 | 
			
		||||
			JOIN json_each(?) AS identities ON identities.value = messages.author
 | 
			
		||||
			WHERE
 | 
			
		||||
				json_extract(messages.content, '$.type') = 'about' AND
 | 
			
		||||
				content ->> 'about' = messages.author AND name IS NOT NULL)
 | 
			
		||||
		WHERE author_rank = 1
 | 
			
		||||
	`, [JSON.stringify(identities)])).map(x => [x.author, x.name]));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function render(hash) {
 | 
			
		||||
	g_hash = hash;
 | 
			
		||||
	if (!hash) {
 | 
			
		||||
		let sites = (await query(`
 | 
			
		||||
			SELECT site.author, site.id
 | 
			
		||||
			FROM messages site
 | 
			
		||||
			WHERE site.content ->> 'type' = 'web-init'
 | 
			
		||||
		`, []));
 | 
			
		||||
		let names = await get_names(sites.map(x => x.author));
 | 
			
		||||
		if (hash === g_hash) {
 | 
			
		||||
			await app.setDocument(`<ul style="background-color: #ddd">${sites.map(x => `<li><a target="_top" href="#${encodeURIComponent(x.id)}">${names[x.author] ?? x.author} - ${x.id}</a></li>`).join('\n')}</ul>`);
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		let site_id = hash.charAt(0) == '#' ? decodeURIComponent(hash.substring(1)) : decodeURIComponent(hash);
 | 
			
		||||
		await app.setDocument(`<html style="margin: 0; padding: 0; width: 100vw; height: 100vh; margin: 0; padding: 0">
 | 
			
		||||
			<body style="display: flex; flex-direction: column; width: 100vw; height: 100vh">
 | 
			
		||||
				<iframe src="${encodeURIComponent(site_id)}/index.html" style="flex: 1 1; border: 0; background-color: #fff"></iframe>
 | 
			
		||||
			</body>
 | 
			
		||||
		</html>`);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
core.register('message', async function message_handler(message) {
 | 
			
		||||
	if (message.event == 'hashChange') {
 | 
			
		||||
		await render(message.hash);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	render(null);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
							
								
								
									
										59
									
								
								apps/web/handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								apps/web/handler.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
async function query(sql, params) {
 | 
			
		||||
	let results = [];
 | 
			
		||||
	await ssb.sqlAsync(sql, params, function(row) {
 | 
			
		||||
		results.push(row);
 | 
			
		||||
	});
 | 
			
		||||
	return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function guess_content_type(name) {
 | 
			
		||||
	if (name.endsWith('.html')) {
 | 
			
		||||
		return 'text/html; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.js') || name.endsWith('.mjs')) {
 | 
			
		||||
		return 'text/javascript; charset=UTF-8';
 | 
			
		||||
	} else if (name.endsWith('.css')) {
 | 
			
		||||
		return 'text/stylesheet; charset=UTF-8';
 | 
			
		||||
	} else {
 | 
			
		||||
		return 'application/binary';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	let path = request.path.replaceAll(/(%[0-9a-fA-F]{2})/g, x => String.fromCharCode(parseInt(x.substring(1), 16)));
 | 
			
		||||
	let match = path.match(/^(%.{44}\.sha256)(?:\/)?(.*)$/);
 | 
			
		||||
 | 
			
		||||
	let content_type = guess_content_type(request.path);
 | 
			
		||||
	let root = await query(`
 | 
			
		||||
		SELECT root.content ->> 'root' AS root
 | 
			
		||||
		FROM messages site
 | 
			
		||||
		JOIN messages root
 | 
			
		||||
		ON site.id = ? AND root.author = site.author AND root.content ->> 'site' = site.id
 | 
			
		||||
		ORDER BY root.sequence DESC LIMIT 1
 | 
			
		||||
	`, [match[1]]);
 | 
			
		||||
	let root_id = root[0]['root'];
 | 
			
		||||
	let last_id = root_id;
 | 
			
		||||
	let blob = await ssb.blobGet(root_id);
 | 
			
		||||
	try {
 | 
			
		||||
		for (let part of match[2]?.split('/')) {
 | 
			
		||||
			let dir = JSON.parse(utf8Decode(blob));
 | 
			
		||||
			last_id = dir?.links[part];
 | 
			
		||||
			blob = await ssb.blobGet(dir?.links[part]);
 | 
			
		||||
			content_type = guess_content_type(part);
 | 
			
		||||
		}
 | 
			
		||||
	} catch {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: blob ? utf8Decode(blob) : `${last_id} not found`,
 | 
			
		||||
		content_type: content_type,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main().catch(function(e) {
 | 
			
		||||
	respond({
 | 
			
		||||
		status_code: 200,
 | 
			
		||||
		data: `${e.message}\n${e.stack}`,
 | 
			
		||||
		content_type: 'text/plain',
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user