audio streaming
							parent
							
								
									4416a3766b
								
							
						
					
					
						commit
						8ef0172d11
					
				|  | @ -1,6 +1,7 @@ | |||
| import { render, html } from '/uhtml.js'; | ||||
| import loadMessages from '/message.js'; | ||||
| import loadSpace from '/space.js'; | ||||
| import loadStreams from '/stream.js'; | ||||
| 
 | ||||
| function setVisibility() { | ||||
| 	document.getElementById('visibility').innerText = `${ | ||||
|  | @ -33,6 +34,7 @@ function chooseThread() { | |||
| 	document.getElementById('threadname').textContent = this.thread.name; | ||||
| 	this.classList.add('active'); | ||||
| 	window.currentThread = this.thread; | ||||
| 	loadStreams(); | ||||
| 	if (this.tab) | ||||
| 		switchTab(document.getElementById(this.tab)); | ||||
| 	else // load first tab that has any content
 | ||||
|  | @ -40,8 +42,16 @@ function chooseThread() { | |||
| 			if (messages.length) | ||||
| 				switchTab(document.getElementById(this.tab = 'messagetab')); | ||||
| 			else | ||||
| 				loadSpace(spans => switchTab( | ||||
| 					document.getElementById(this.tab = spans.length ? 'spacetab' : 'messagetab'))); | ||||
| 				loadSpace(spans => { | ||||
| 					if (spans) | ||||
| 						switchTab(document.getElementById(this.tab = 'spacetab')); | ||||
| 					else if (window.currentThread.streams.length) | ||||
| 						switchTab(document.getElementById(this.tab = 'streamtab')); | ||||
| 					else | ||||
| 						switchTab(document.getElementById( | ||||
| 							this.tab = spans.length ? 'spacetab' : | ||||
| 								window.currentThread.streams.length ? 'streamtab' : 'messagetab')); | ||||
| 				}); | ||||
| 		}); | ||||
| 	window.emit('get_thread', { thread: this.thread.id }, msg => { | ||||
| 		window.currentThread = msg.thread; | ||||
|  | @ -148,7 +158,6 @@ function newThread() { | |||
| 			<input type='radio' name='newpermissions' | ||||
| 				id='private_view' value='private_view' /> | ||||
| 			<label for='private_view'>only members can view and post</label><br> | ||||
| 			<br> | ||||
| 			<label class='heading' for='membername'>members</label> | ||||
| 			<input type='text' id='membername' placeholder='username' onkeydown=${event => { | ||||
| 				if (event.key === 'Enter') { | ||||
|  | @ -267,7 +276,8 @@ document.body.append(html.node` | |||
| 			<div id='buttonbar'> | ||||
| 				<div id='tabs'> | ||||
| 					<button id='messagetab' class='tab active' onclick=${clickedTab}>messages | ||||
| 					</button><button id='spacetab' class='tab' onclick=${clickedTab}>space</button> | ||||
| 					</button><button id='spacetab' class='tab' onclick=${clickedTab}>space | ||||
| 					</button><button id='streamtab' class='tab' onclick=${clickedTab}>streams</button> | ||||
| 				</div> | ||||
| 				<button id='showmembers' onclick=${() => | ||||
| 					document.getElementById('members').classList.toggle('hidden') | ||||
|  | @ -275,6 +285,7 @@ document.body.append(html.node` | |||
| 			</div> | ||||
| 			<div id='message' class='tabcontent'></div> | ||||
| 			<div id='space' class='tabcontent hidden'></div> | ||||
| 			<div id='stream' class='tabcontent hidden'></div> | ||||
| 		</div> | ||||
| 		<hr class='separator' color='#505050'> | ||||
| 		<div id='members' class='hidden'> | ||||
|  |  | |||
|  | @ -81,8 +81,11 @@ | |||
| 				background-color: #4f4f4f; | ||||
| 				color: #fff; | ||||
| 			} | ||||
| 			p { | ||||
| 				margin: 5px 1px; | ||||
| 			} | ||||
| 			label.heading { | ||||
| 				margin-bottom: 5px; | ||||
| 				margin: 10px 1px 4px; | ||||
| 				display: block; | ||||
| 			} | ||||
| 			h3 { | ||||
|  | @ -218,9 +221,6 @@ | |||
| 				white-space: pre-line; | ||||
| 				margin-bottom: 12px; | ||||
| 			} | ||||
| 			.member { | ||||
| 				margin: 5px 0; | ||||
| 			} | ||||
| 			#space { | ||||
| 				margin: -2px; /* offset column margin */ | ||||
| 				position: relative; | ||||
|  | @ -230,6 +230,13 @@ | |||
| 				position: absolute; | ||||
| 				white-space: nowrap; | ||||
| 			} | ||||
| 			#stream { | ||||
| 				margin: 4px 2px; | ||||
| 			} | ||||
| 			#play { | ||||
| 				line-height: 1.4; | ||||
| 				font-family: 'Segoe UI Symbol', math; | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body></body> | ||||
|  |  | |||
|  | @ -0,0 +1,162 @@ | |||
| import { render, html } from '/uhtml.js'; | ||||
| 
 | ||||
| let streamid; | ||||
| 
 | ||||
| let handle; | ||||
| let mediaStream; | ||||
| let recorder; | ||||
| 
 | ||||
| async function stream() { | ||||
| 	if (handle) { | ||||
| 		clearInterval(handle); | ||||
| 		handle = null; | ||||
| 		if (recorder.state === 'recording') | ||||
| 			recorder.stop(); | ||||
| 		window.emit('stream', { | ||||
| 			id: streamid, | ||||
| 			thread: window.currentThread.id, | ||||
| 			stop: true | ||||
| 		}); | ||||
| 		document.getElementById('streaming').innerText = 'start streaming'; | ||||
| 		return; | ||||
| 	} | ||||
| 	if (!mediaStream) | ||||
| 		mediaStream = await navigator.mediaDevices.getUserMedia({ | ||||
| 			audio: { | ||||
| 				autoGainControl: false, | ||||
| 				echoCancellation: false, | ||||
| 				noiseSuppression: false, | ||||
| 				sampleRate: 48000, | ||||
| 				sampleSize: 16 | ||||
| 			} | ||||
| 		}); | ||||
| 	if (!mediaStream) | ||||
| 		return; | ||||
| 	window.emit('stream', { | ||||
| 		thread: window.currentThread.id, | ||||
| 		name: document.getElementById('streamname').value | ||||
| 	}, msg => { | ||||
| 		if (!msg.success) { | ||||
| 			console.log('stream failed: ', msg.message); | ||||
| 			return; | ||||
| 		} | ||||
| 		streamid = msg.id; | ||||
| 		document.getElementById('streaming').innerText = 'stop streaming'; | ||||
| 	}); | ||||
| 	function record() { | ||||
| 		let r = recorder = new MediaRecorder(mediaStream); | ||||
| 		let chunks = []; | ||||
| 		r.ondataavailable = event => { | ||||
| 			if (!event.data.size) | ||||
| 				return; | ||||
| 			chunks.push(event.data); | ||||
| 		}; | ||||
| 		r.onstop = async () => { | ||||
| 			if (!chunks.length || !handle) | ||||
| 				return; | ||||
| 			//console.log(`${Date.now()} ${chunks.length}`);
 | ||||
| 			window.emit('streamdata', { | ||||
| 				id: streamid, | ||||
| 				audio: await (new Blob(chunks, { type: chunks[0].type })).arrayBuffer() | ||||
| 			}, msg => { | ||||
| 				if (!msg.success) | ||||
| 					console.log('streamdata failed: ', msg.message); | ||||
| 			}); | ||||
| 		}; | ||||
| 		r.onstart = () => { | ||||
| 			setTimeout(() => r.state === 'recording' && r.stop(), 100); | ||||
| 		}; | ||||
| 		r.start(); | ||||
| 	} | ||||
| 	record(); | ||||
| 	handle = setInterval(record, 100); | ||||
| } | ||||
| 
 | ||||
| let audioctx; | ||||
| let streaming = {}; | ||||
| 
 | ||||
| function addStream(stream) { | ||||
| 	let p = html.node` | ||||
| 		<p> | ||||
| 			<button id='play' onclick=${e => { | ||||
| 				if (stream.playing) { | ||||
| 					audioctx.suspend(); | ||||
| 					delete streaming[stream.id]; | ||||
| 					stream.playing = false; | ||||
| 					e.target.innerText = '▶'; | ||||
| 				} | ||||
| 				else { | ||||
| 					audioctx = new AudioContext(); | ||||
| 					streaming[stream.id] = stream; | ||||
| 					stream.playing = true; | ||||
| 					e.target.innerText = '⏹'; | ||||
| 				} | ||||
| 				window.emit('play_stream', { | ||||
| 					id: stream.id, | ||||
| 					thread: window.currentThread.id, | ||||
| 					playing: stream.playing | ||||
| 				}, msg => { | ||||
| 					if (!msg.success) | ||||
| 						console.log('play stream failed: ', msg.message); | ||||
| 				}); | ||||
| 			}}>▶</button> | ||||
| 			${stream.user}<span id='name'>${stream.name ? ` - ${stream.name}` : ''}</span> | ||||
| 		</p>`; | ||||
| 	p.id = 'stream' + stream.id; | ||||
| 	document.getElementById('streams').append(p); | ||||
| } | ||||
| 
 | ||||
| function loadStreams() { | ||||
| 	let div = document.getElementById('stream'); | ||||
| 	div.innerHTML = ''; | ||||
| 	if (window.currentThread.permissions.post) { | ||||
| 		// why doesn't html` work here? html.node` does
 | ||||
| 		render(div, html.node` | ||||
| 			<button id='streaming' onclick=${stream}>start streaming</button> | ||||
| 			<span>stream name:</span> | ||||
| 			<input id='streamname'>`);
 | ||||
| 		div.children['streamname'].oninput = event => { | ||||
| 			if (handle) | ||||
| 				window.emit('stream', { | ||||
| 					id: streamid, | ||||
| 					thread: window.currentThread.id, | ||||
| 					name: div.children['streamname'].value | ||||
| 				}); | ||||
| 		}; | ||||
| 	} | ||||
| 	div.insertAdjacentHTML('beforeend', ` | ||||
| 		<p>streams:</p> | ||||
| 		<div id='streams'></div>`); | ||||
| 	for (let stream of window.currentThread.streams) | ||||
| 		addStream(stream); | ||||
| } | ||||
| 
 | ||||
| window.socket.on('stream', async msg => { | ||||
| 	if (msg.thread !== window.currentThread.id) | ||||
| 		return; | ||||
| 	let p = document.getElementById('stream' + msg.id); | ||||
| 	if (p) { | ||||
| 		if (msg.stopped) { | ||||
| 			p.remove(); | ||||
| 			window.currentThread.streams.splice( | ||||
| 				window.currentThread.streams.findIndex(s => s.id === msg.id), 1); | ||||
| 		} | ||||
| 		else | ||||
| 			p.children['name'].innerText = msg.name ? ' - ' + msg.name : ''; | ||||
| 	} | ||||
| 	else if (!msg.stopped) { | ||||
| 		window.currentThread.streams.push(msg); | ||||
| 		addStream(msg); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| window.socket.on('streamdata', async msg => { | ||||
| 	if (!streaming[msg.id]) | ||||
| 		return; | ||||
| 	let source = audioctx.createBufferSource(); | ||||
| 	source.buffer = await audioctx.decodeAudioData(msg.audio); | ||||
| 	source.connect(audioctx.destination); | ||||
| 	source.start(/*audioStartTime*/); | ||||
| }); | ||||
| 
 | ||||
| export default loadStreams; | ||||
							
								
								
									
										43
									
								
								index.js
								
								
								
								
							
							
						
						
									
										43
									
								
								index.js
								
								
								
								
							|  | @ -4,10 +4,12 @@ const http = require('http'); | |||
| const { Server } = require('socket.io'); | ||||
| const compression = require('compression'); | ||||
| 
 | ||||
| const events = Object.fromEntries( | ||||
| 	fs.readdirSync('./src/event') | ||||
| 		.map(event => [event.slice(0, -3), require('./src/event/' + event)]) | ||||
| ); | ||||
| const events = {}; | ||||
| for (let file of fs.readdirSync('./src/events')) { | ||||
| 	file = require('./src/events/' + file); | ||||
| 	for (const event in file) | ||||
| 		events[event] = file[event]; | ||||
| } | ||||
| 
 | ||||
| const PORT = process.env.PORT || 3435; | ||||
| 
 | ||||
|  | @ -16,10 +18,18 @@ app.use(compression()); | |||
| const server = http.createServer(app); | ||||
| const io = new Server(server, { | ||||
| 	cors: { | ||||
| 		origin: true, | ||||
| 	}, | ||||
| 		origin: true | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| app.use(express.static('client')); | ||||
| 
 | ||||
| global.vybe = { | ||||
| 	users: {}, | ||||
| 	threads: {}, | ||||
| 	streams: {} | ||||
| }; | ||||
| 
 | ||||
| io.on('connection', (socket) => { | ||||
| 	for (let event in events) { | ||||
| 		socket.on(event, (msg, callback) => { | ||||
|  | @ -27,22 +37,27 @@ io.on('connection', (socket) => { | |||
| 				callback('no such event ' + event); | ||||
| 				return; | ||||
| 			} | ||||
| 			events[event](msg, callback, socket); | ||||
| 			try { | ||||
| 				events[event](msg, callback, socket); | ||||
| 			} | ||||
| 			catch (e) { | ||||
| 				console.log(`${event} threw exception: `, e); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 	socket.on('disconnect', reason => { | ||||
| 		let user = vybe.users[socket.username]; | ||||
| 		if (user) | ||||
| 			user.sockets.splice(user.sockets.indexOf(socket), 1); | ||||
| 	}) | ||||
| 		for (let id in vybe.streams) { | ||||
| 			const stream = vybe.streams[id]; | ||||
| 			delete stream.listeners[socket.id]; | ||||
| 			if (stream.socket === socket.id) | ||||
| 				stream.stop(); | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| global.vybe = { | ||||
| 	users: {} | ||||
| }; | ||||
| 
 | ||||
| server.listen(PORT, () => { | ||||
| 	console.log('server running on port ' + PORT); | ||||
| }); | ||||
| 
 | ||||
| app.use(express.static('client')); | ||||
|  |  | |||
|  | @ -1,119 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| 
 | ||||
| const create_thread = async (msg, respond) => { | ||||
| 	// validate inputs
 | ||||
| 	if (typeof msg.name !== 'string') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (msg.name.length > 200) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name 200 chars max' | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		'insert into thread (name, creator) values (?, ?) returning id', | ||||
| 		[msg.name, msg.auth_user.id] | ||||
| 	); | ||||
| 	const thread_id = insert.rows[0].id; | ||||
| 	// set up permissions
 | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'everyone', true, 'view', 'true'] | ||||
| 		); | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, 'everyone', true, 'post', 'true'] | ||||
| 			); | ||||
| 		} else { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, 'members', true, 'post', 'true'] | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'members', true, 'view', 'true'] | ||||
| 		); | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'members', true, 'post', 'true'] | ||||
| 		); | ||||
| 	} | ||||
| 	// add members
 | ||||
| 	if (Array.isArray(msg.members)) { | ||||
| 		for (let member of msg.members) { | ||||
| 			if (!member) continue; | ||||
| 			// get user id
 | ||||
| 			const id = await db.query('select id from user where name = ?', [ | ||||
| 				member.name, | ||||
| 			]); | ||||
| 			if (id.rows.length === 0) { | ||||
| 				console.log('user not found: ' + member.name); | ||||
| 				continue; | ||||
| 			} | ||||
| 			await db.query( | ||||
| 				'insert into member (thread, user) values (?, ?)', | ||||
| 				[thread_id, id.rows[0].id] | ||||
| 			); | ||||
| 			if (typeof member.permissions === 'object') | ||||
| 				for (let permission in member.permissions) | ||||
| 					await db.query(` | ||||
| 						insert into permission (thread, type, user, mutable, permission, value) | ||||
| 						values (?, ?, ?, ?, ?, ?)`,
 | ||||
| 						[thread_id, 'user', id.rows[0].id, true, permission, member.permissions[permission]]); | ||||
| 		} | ||||
| 	} | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		for (let username in vybe.users) { | ||||
| 			for (let socket of vybe.users[username].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: false, | ||||
| 						view: true, | ||||
| 						post: !msg.permissions || !msg.permissions.post_limited | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	else if (Array.isArray(msg.members)) { | ||||
| 		for (let member of msg.members) { | ||||
| 			if (!vybe.users[member.name]) | ||||
| 				continue; | ||||
| 			for (let socket of vybe.users[member.name].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: true, | ||||
| 						view: true, | ||||
| 						post: true | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: thread_id | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(create_thread); | ||||
|  | @ -1,55 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const openpgp = require('openpgp'); | ||||
| 
 | ||||
| const create_user = async (msg, respond) => { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.name) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'username required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!msg.pubkey) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'public key required' | ||||
| 		}); | ||||
| 	} | ||||
| 	// ensure username is not taken
 | ||||
| 	const result = await db.query('select * from user where name = ?', [ | ||||
| 		msg.name | ||||
| 	]); | ||||
| 	if (result.rows.length > 0) { | ||||
| 		console.log(`username already exists: ${result}`); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'a user with this name already exists on this server' | ||||
| 		}); | ||||
| 	} | ||||
| 	// validate public key
 | ||||
| 	try { | ||||
| 		await openpgp.readKey({ armoredKey: msg.pubkey }); | ||||
| 	} catch (err) { | ||||
| 		console.err('error in create_user readkey: ' + err); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'public key invalid' | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		'insert into user (name) values (?) returning id', | ||||
| 		[msg.name] | ||||
| 	); | ||||
| 	await db.query( | ||||
| 		'insert into key (user, pubkey, active) values (?, ?, true)', | ||||
| 		[insert.rows[0].id, msg.pubkey] | ||||
| 	) | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: insert.rows[0].id | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = create_user; | ||||
|  | @ -1,155 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const edit_thread = async (msg, respond) => { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.id || typeof msg.name !== 'string') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'invalid msg' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (msg.name.length > 200) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name 200 chars max' | ||||
| 		}); | ||||
| 	} | ||||
| 	const perms = await check_permission(msg.auth_user.id, msg.id); | ||||
| 	if (!perms.admin) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "user doesn't have permission" | ||||
| 		}); | ||||
| 	} | ||||
| 	// update name
 | ||||
| 	await db.query( | ||||
| 		'update thread set name = ? where id = ?', | ||||
| 		[msg.name, msg.id] | ||||
| 	); | ||||
| 	// update permissions
 | ||||
| 	let permissions = {}; | ||||
| 	for (const p of (await db.query( | ||||
| 		`select type, permission, value, mutable
 | ||||
| 		from permission | ||||
| 		where type != 'user' and thread = ?`,
 | ||||
| 		[msg.id] | ||||
| 	)).rows) { | ||||
| 		(permissions[p.type] || (permissions[p.type] = {})) | ||||
| 			[p.permission] = { | ||||
| 				value: p.value, | ||||
| 				mutable: p.mutable | ||||
| 			}; | ||||
| 	} | ||||
| 	async function setPermission(type, permission, value) { | ||||
| 		if (permissions[type] && permissions[type][permission]) { | ||||
| 			if (!permissions[type][permission].mutable) { | ||||
| 				respond({ | ||||
| 					success: false, | ||||
| 					message: 'permission not mutable' | ||||
| 				}); | ||||
| 				return false; | ||||
| 			} | ||||
| 			if (permissions[type][permission].value !== value) { | ||||
| 				await db.query(` | ||||
| 					update permission set value = ? | ||||
| 					where thread = ? and type = ? and permission = ?`,
 | ||||
| 					[value, msg.id, type, permission] | ||||
| 				); | ||||
| 				permissions[type][permission].value = value; | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[msg.id, type, true, permission, value] | ||||
| 			); | ||||
| 			(permissions[type] || (permissions[type] = {})) | ||||
| 				[permission] = { | ||||
| 					value, | ||||
| 					mutable: true | ||||
| 				}; | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		if (!await setPermission('everyone', 'view', 'true')) | ||||
| 			return; | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			if (!await setPermission('everyone', 'post', 'true')) | ||||
| 				return; | ||||
| 		} | ||||
| 		else { | ||||
| 			if (!await setPermission('members', 'post', 'true')) | ||||
| 				return; | ||||
| 			if (!await setPermission('everyone', 'post', 'false')) | ||||
| 				return; | ||||
| 		} | ||||
| 	} else { | ||||
| 		if (!await setPermission('members', 'view', 'true')) | ||||
| 			return; | ||||
| 		if (!await setPermission('members', 'post', 'true')) | ||||
| 			return; | ||||
| 		if (!await setPermission('everyone', 'view', 'false')) | ||||
| 			return; | ||||
| 		if (!await setPermission('everyone', 'post', 'false')) | ||||
| 			return; | ||||
| 	} | ||||
| 	permissions.everyone.view.value = permissions.everyone.view.value === 'true'; | ||||
| 	permissions.everyone.post.value = permissions.everyone.post.value === 'true'; | ||||
| 	if (!permissions.members) permissions.members = {}; | ||||
| 	if (!permissions.members.view) | ||||
| 		permissions.members.view = { value: 'true', mutable: true }; | ||||
| 	if (!permissions.members.post) | ||||
| 		permissions.members.post = { value: 'true', mutable: true }; | ||||
| 	permissions.members.view.value = permissions.members.view.value === 'true'; | ||||
| 	permissions.members.post.value = permissions.members.post.value === 'true'; | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		for (let username in vybe.users) { | ||||
| 			for (let socket of vybe.users[username].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: msg.id, | ||||
| 					permissions: { | ||||
| 						is_member: false, | ||||
| 						view: true, | ||||
| 						post: !msg.permissions || !msg.permissions.post_limited, | ||||
| 						...permissions | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	else { | ||||
| 		for (let member of (await db.query( | ||||
| 			`select user.name, user.id from thread
 | ||||
| 			join member on thread.id = member.thread | ||||
| 			join user on user.id = member.user | ||||
| 			where thread.id = ?`,
 | ||||
| 			[msg.id] | ||||
| 		)).rows) { | ||||
| 			if (!vybe.users[member.name]) | ||||
| 				continue; | ||||
| 			for (let socket of vybe.users[member.name].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: msg.id, | ||||
| 					permissions: { | ||||
| 						is_member: true, | ||||
| 						view: true, | ||||
| 						post: true, | ||||
| 						admin: member.id === msg.auth_user.id && perms.admin, | ||||
| 						...permissions | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return respond({ | ||||
| 		success: true | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(edit_thread); | ||||
|  | @ -1,42 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const get_history = async (msg, respond) => { | ||||
| 	if (msg.before && isNaN(Number(msg.before))) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'before must be a number', | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required', | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread", | ||||
| 		}); | ||||
| 	} | ||||
| 	const messages = await db.query( | ||||
| 		`select user.name, post.id, content from post
 | ||||
| 		join user on post.user = user.id | ||||
| 		${msg.before ? 'where post.id < ? and' : 'where'} | ||||
| 		thread = ? | ||||
| 		order by post.created desc | ||||
| 		limit 101`,
 | ||||
| 		msg.before ? [msg.before, msg.thread] : [msg.thread] | ||||
| 	); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		messages: messages.rows | ||||
| 			.slice(0, 100) | ||||
| 			.map((i) => ({ id: i.id, name: i.name, message: i.content })), | ||||
| 		more: messages.rows.length > 100, | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_history); | ||||
|  | @ -1,30 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| 
 | ||||
| const get_keys = async (msg, respond) => { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.names) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'user names required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (typeof msg.names !== 'object') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "can't iterate user names" | ||||
| 		}); | ||||
| 	} | ||||
| 	const keys = await db.query( // todo: encryption !
 | ||||
| 		`select name from user where name in
 | ||||
| 			(${msg.names.map((i) => '?').join(',')})`,
 | ||||
| 		msg.names | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		keys: keys.rows | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_keys); | ||||
|  | @ -1,28 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const get_space = async (msg, respond) => { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread" | ||||
| 		}); | ||||
| 	} | ||||
| 	const spans = await db.query( | ||||
| 		'select id, content, x, y, scale from span where thread = ? and deleted = false', | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		spans: spans.rows | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_space); | ||||
|  | @ -1,76 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const get_thread = async (msg, respond) => { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required' | ||||
| 		}); | ||||
| 	} | ||||
| 	let perms = await check_permission(msg.auth_user.id, msg.thread); | ||||
| 	if (!perms.view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread" | ||||
| 		}); | ||||
| 	} | ||||
| 	const thread = await db.query( | ||||
| 		`select thread.name, user.name as user, user.id from thread
 | ||||
| 		left join member on thread.id = member.thread | ||||
| 		left join user on user.id = member.user | ||||
| 		where thread.id = ?`,
 | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	const permissions = await db.query( | ||||
| 		`select type, user, permission, value, mutable
 | ||||
| 		from permission where thread = ?`,
 | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	let members = Object.fromEntries(thread.rows.map(member => | ||||
| 		[member.id, { name: member.user }] | ||||
| 	)); | ||||
| 	for (let permission of permissions.rows) { | ||||
| 		const member = members[permission.user]; | ||||
| 		if (member) | ||||
| 			(member.permissions || (member.permissions = {})) | ||||
| 				[permission.permission] = { | ||||
| 					value: permission.value, | ||||
| 					mutable: permission.mutable | ||||
| 				}; | ||||
| 		else | ||||
| 			(perms[permission.type] || (perms[permission.type] = {})) | ||||
| 				[permission.permission] = { | ||||
| 					value: permission.value, | ||||
| 					mutable: permission.mutable | ||||
| 				}; | ||||
| 	} | ||||
| 	function makeBool(type, permission) { | ||||
| 		if (perms[type]) { | ||||
| 			if (perms[type][permission]) | ||||
| 				perms[type][permission].value = perms[type][permission].value === 'true'; | ||||
| 			else | ||||
| 				perms[type][permission] = { value: false, mutable: true }; | ||||
| 		} else | ||||
| 			(perms[type] = {})[permission] = { value: false, mutable: true }; | ||||
| 	} | ||||
| 	makeBool('everyone', 'view'); | ||||
| 	makeBool('everyone', 'post'); | ||||
| 	makeBool('members', 'view'); | ||||
| 	makeBool('members', 'post'); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		thread: { | ||||
| 			id: msg.thread, | ||||
| 			name: thread.rows[0].name, | ||||
| 			permissions: perms, | ||||
| 			members: Object.entries(members).map(member => ({ | ||||
| 				id: member[0], | ||||
| 				...member[1] | ||||
| 			})) | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(get_thread); | ||||
|  | @ -1,32 +0,0 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const list_threads = async (msg, respond) => { | ||||
| 	const threads = await db.query( | ||||
| 		`select name, id from thread
 | ||||
| 		join permission on thread.id = permission.thread | ||||
| 		left join member on thread.id = member.thread | ||||
| 		where permission.permission = 'view' | ||||
| 		and permission.value = 'true' | ||||
| 		and ((permission.type = 'everyone') or | ||||
| 		permission.type = 'members' and member.user = ?) | ||||
| 		group by thread.id | ||||
| 		order by thread.created desc`,
 | ||||
| 		[msg.auth_user.id] | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	const rows = []; | ||||
| 	for (let thread of threads.rows) { | ||||
| 		rows.push({ | ||||
| 			...thread, | ||||
| 			permissions: await check_permission(msg.auth_user.id, thread.id) | ||||
| 		}); | ||||
| 	} | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		threads: rows | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| module.exports = authwrap(list_threads); | ||||
|  | @ -2,7 +2,7 @@ const db = require('../db'); | |||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const send_message = async (msg, respond) => { | ||||
| async function send_message(msg, respond) { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
|  | @ -30,8 +30,7 @@ const send_message = async (msg, respond) => { | |||
| 	// get perms
 | ||||
| 	const permissions = await db.query( | ||||
| 		"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'", | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 		[msg.thread]); | ||||
| 	for (let username in vybe.users) { | ||||
| 		if (permissions.rows.length > 0 || members.includes(username)) { | ||||
| 			for (let s of vybe.users[username].sockets) { | ||||
|  | @ -48,6 +47,46 @@ const send_message = async (msg, respond) => { | |||
| 		success: true, | ||||
| 		id: id.rows[0].id | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| module.exports = authwrap(send_message); | ||||
| async function get_history(msg, respond) { | ||||
| 	if (msg.before && isNaN(Number(msg.before))) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'before must be a number' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread" | ||||
| 		}); | ||||
| 	} | ||||
| 	const messages = await db.query( | ||||
| 		`select user.name, post.id, content from post
 | ||||
| 		join user on post.user = user.id | ||||
| 		${msg.before ? 'where post.id < ? and' : 'where'} | ||||
| 		thread = ? | ||||
| 		order by post.created desc | ||||
| 		limit 101`,
 | ||||
| 		msg.before ? [msg.before, msg.thread] : [msg.thread] | ||||
| 	); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		messages: messages.rows | ||||
| 			.slice(0, 100) | ||||
| 			.map((i) => ({ id: i.id, name: i.name, message: i.content })), | ||||
| 		more: messages.rows.length > 100 | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	send_message: authwrap(send_message), | ||||
| 	get_history: authwrap(get_history) | ||||
| }; | ||||
|  | @ -2,7 +2,30 @@ const db = require('../db'); | |||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| const save_span = async (msg, respond, socket) => { | ||||
| async function get_space(msg, respond) { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread" | ||||
| 		}); | ||||
| 	} | ||||
| 	const spans = await db.query( | ||||
| 		'select id, content, x, y, scale from span where thread = ? and deleted = false', | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		spans: spans.rows | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function save_span(msg, respond, socket) { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
|  | @ -62,6 +85,9 @@ const save_span = async (msg, respond, socket) => { | |||
| 		success: true, | ||||
| 		id | ||||
| 	}); | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| module.exports = authwrap(save_span); | ||||
| module.exports = { | ||||
| 	get_space: authwrap(get_space), | ||||
| 	save_span: authwrap(save_span) | ||||
| }; | ||||
|  | @ -0,0 +1,132 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| let newstreamid = 0; | ||||
| 
 | ||||
| async function stream(msg, respond, socket) { | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).post) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "user doesn't have permission" | ||||
| 		}); | ||||
| 	} | ||||
| 	let stream; | ||||
| 	let thread = vybe.threads[msg.thread] || (vybe.threads[msg.thread] = { | ||||
| 		streams: [] | ||||
| 	}); | ||||
| 	async function send() { | ||||
| 		const permissions = await db.query( | ||||
| 			"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'", | ||||
| 			[msg.thread]); | ||||
| 		if (permissions.rows.length) { | ||||
| 			for (let user in vybe.users) { | ||||
| 				user = vybe.users[user]; | ||||
| 				for (let socket of user.sockets) | ||||
| 					socket.emit('stream', stream); | ||||
| 			} | ||||
| 		} else { | ||||
| 			const members = ( | ||||
| 				await db.query( | ||||
| 					'select name from user join member on member.user = user.id where member.thread = ?', | ||||
| 					[msg.thread] | ||||
| 				) | ||||
| 			).rows.map((i) => i.name); | ||||
| 			for (let member of members) { | ||||
| 				let user = vybe.users[member]; | ||||
| 				if (user) | ||||
| 					for (let socket of user.sockets) | ||||
| 						socket.emit('stream', stream); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if (msg.id) { | ||||
| 		stream = vybe.streams[msg.id]; | ||||
| 		if (!stream) | ||||
| 			return respond({ | ||||
| 				success: false, | ||||
| 				message: 'stream not found' | ||||
| 			}); | ||||
| 		if (msg.stop) { | ||||
| 			stream.stop(); | ||||
| 			return respond({ | ||||
| 				success: true | ||||
| 			}); | ||||
| 		} | ||||
| 		stream = stream.stream; | ||||
| 		stream.name = msg.name; | ||||
| 	} | ||||
| 	else { | ||||
| 		stream = { | ||||
| 			id: newstreamid++, | ||||
| 			thread: msg.thread, | ||||
| 			user: msg.auth_user.name, | ||||
| 			name: msg.name | ||||
| 		}; | ||||
| 		thread.streams.push(stream); | ||||
| 		vybe.streams[stream.id] = { | ||||
| 			stream, | ||||
| 			listeners: {}, | ||||
| 			socket: socket.id, | ||||
| 			stop: () => { | ||||
| 				stream.stopped = true; | ||||
| 				thread.streams.splice(thread.streams.findIndex(s => s.id === stream.id), 1); | ||||
| 				delete vybe.streams[stream.id]; | ||||
| 				send(); | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	await send(); | ||||
| 	respond({ | ||||
| 		success: true, | ||||
| 		id: stream.id | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function streamdata(msg, respond) { | ||||
| 	let stream = vybe.streams[msg.id]; | ||||
| 	if (!stream) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'stream not found' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (stream.stream.user !== msg.auth_user.name) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "stream doesn't belong to user" | ||||
| 		}); | ||||
| 	} | ||||
| 	for (let id in stream.listeners) | ||||
| 		stream.listeners[id].emit('streamdata', msg); | ||||
| 	respond({ | ||||
| 		success: true | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function play_stream(msg, respond, socket) { | ||||
| 	if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "user doesn't have permission" | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!vybe.streams[msg.id]) | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'stream not found' | ||||
| 		}); | ||||
| 	if (msg.playing) | ||||
| 		vybe.streams[msg.id].listeners[socket.id] = socket; | ||||
| 	else | ||||
| 		delete vybe.streams[msg.id].listeners[socket.id]; | ||||
| 	respond({ | ||||
| 		success: true | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	stream: authwrap(stream), | ||||
| 	streamdata: authwrap(streamdata), | ||||
| 	play_stream: authwrap(play_stream) | ||||
| }; | ||||
|  | @ -0,0 +1,377 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const check_permission = require('../check_permission'); | ||||
| 
 | ||||
| async function create_thread(msg, respond) { | ||||
| 	// validate inputs
 | ||||
| 	if (typeof msg.name !== 'string') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (msg.name.length > 200) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name 200 chars max' | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		'insert into thread (name, creator) values (?, ?) returning id', | ||||
| 		[msg.name, msg.auth_user.id] | ||||
| 	); | ||||
| 	const thread_id = insert.rows[0].id; | ||||
| 	// set up permissions
 | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'everyone', true, 'view', 'true'] | ||||
| 		); | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, 'everyone', true, 'post', 'true'] | ||||
| 			); | ||||
| 		} else { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[thread_id, 'members', true, 'post', 'true'] | ||||
| 			); | ||||
| 		} | ||||
| 	} else { | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'members', true, 'view', 'true'] | ||||
| 		); | ||||
| 		await db.query( | ||||
| 			`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 			values (?, ?, ?, ?, ?)`,
 | ||||
| 			[thread_id, 'members', true, 'post', 'true'] | ||||
| 		); | ||||
| 	} | ||||
| 	// add members
 | ||||
| 	if (Array.isArray(msg.members)) { | ||||
| 		for (let member of msg.members) { | ||||
| 			if (!member) continue; | ||||
| 			// get user id
 | ||||
| 			const id = await db.query('select id from user where name = ?', [ | ||||
| 				member.name, | ||||
| 			]); | ||||
| 			if (id.rows.length === 0) { | ||||
| 				console.log('user not found: ' + member.name); | ||||
| 				continue; | ||||
| 			} | ||||
| 			await db.query( | ||||
| 				'insert into member (thread, user) values (?, ?)', | ||||
| 				[thread_id, id.rows[0].id] | ||||
| 			); | ||||
| 			if (typeof member.permissions === 'object') | ||||
| 				for (let permission in member.permissions) | ||||
| 					await db.query(` | ||||
| 						insert into permission (thread, type, user, mutable, permission, value) | ||||
| 						values (?, ?, ?, ?, ?, ?)`,
 | ||||
| 						[thread_id, 'user', id.rows[0].id, true, permission, member.permissions[permission]]); | ||||
| 		} | ||||
| 	} | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		for (let username in vybe.users) { | ||||
| 			for (let socket of vybe.users[username].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: false, | ||||
| 						view: true, | ||||
| 						post: !msg.permissions || !msg.permissions.post_limited | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	else if (Array.isArray(msg.members)) { | ||||
| 		for (let member of msg.members) { | ||||
| 			if (!vybe.users[member.name]) | ||||
| 				continue; | ||||
| 			for (let socket of vybe.users[member.name].sockets) { | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: insert.rows[0].id, | ||||
| 					permissions: { | ||||
| 						is_member: true, | ||||
| 						view: true, | ||||
| 						post: true | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: thread_id | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function list_threads(msg, respond) { | ||||
| 	// this can be optimized by merging the permission check into here
 | ||||
| 	let threads = await db.query( | ||||
| 		`select name, id from thread
 | ||||
| 		join permission on thread.id = permission.thread | ||||
| 		left join member on thread.id = member.thread | ||||
| 		where permission.permission = 'view' | ||||
| 		and permission.value = 'true' | ||||
| 		and ((permission.type = 'everyone') or | ||||
| 		permission.type = 'members' and member.user = ?) | ||||
| 		group by thread.id | ||||
| 		order by thread.created desc`,
 | ||||
| 		[msg.auth_user.id] | ||||
| 	); | ||||
| 	threads = await Promise.all(threads.rows.map(async thread => { | ||||
| 		if (vybe.threads[thread.id]) | ||||
| 			Object.assign(thread, vybe.threads[thread.id]); | ||||
| 		else | ||||
| 			thread.streams = []; | ||||
| 		thread.permissions = await check_permission(msg.auth_user.id, thread.id); | ||||
| 		return thread; | ||||
| 	})); | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		threads | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function get_thread(msg, respond) { | ||||
| 	if (!msg.thread) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread ID required' | ||||
| 		}); | ||||
| 	} | ||||
| 	let perms = await check_permission(msg.auth_user.id, msg.thread); | ||||
| 	if (!perms.view) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "you can't view this thread" | ||||
| 		}); | ||||
| 	} | ||||
| 	let thread = await db.query( | ||||
| 		`select thread.name, user.name as user, user.id from thread
 | ||||
| 		left join member on thread.id = member.thread | ||||
| 		left join user on user.id = member.user | ||||
| 		where thread.id = ?`,
 | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	const permissions = await db.query( | ||||
| 		`select type, user, permission, value, mutable
 | ||||
| 		from permission where thread = ?`,
 | ||||
| 		[msg.thread] | ||||
| 	); | ||||
| 	let members = Object.fromEntries(thread.rows.map(member => | ||||
| 		[member.id, { name: member.user }] | ||||
| 	)); | ||||
| 	for (let permission of permissions.rows) { | ||||
| 		const member = members[permission.user]; | ||||
| 		if (member) | ||||
| 			(member.permissions || (member.permissions = {})) | ||||
| 				[permission.permission] = { | ||||
| 					value: permission.value, | ||||
| 					mutable: permission.mutable | ||||
| 				}; | ||||
| 		else | ||||
| 			(perms[permission.type] || (perms[permission.type] = {})) | ||||
| 				[permission.permission] = { | ||||
| 					value: permission.value, | ||||
| 					mutable: permission.mutable | ||||
| 				}; | ||||
| 	} | ||||
| 	function makeBool(type, permission, fallback) { | ||||
| 		if (perms[type]) { | ||||
| 			if (perms[type][permission]) | ||||
| 				perms[type][permission].value = perms[type][permission].value === 'true'; | ||||
| 			else | ||||
| 				perms[type][permission] = { value: fallback, mutable: true }; | ||||
| 		} else | ||||
| 			(perms[type] = {})[permission] = { value: fallback, mutable: true }; | ||||
| 	} | ||||
| 	makeBool('everyone', 'view', false); | ||||
| 	makeBool('everyone', 'post', false); | ||||
| 	makeBool('members', 'view', true); | ||||
| 	makeBool('members', 'post', true); | ||||
| 	thread = { | ||||
| 		id: msg.thread, | ||||
| 		name: thread.rows[0].name, | ||||
| 		permissions: perms, | ||||
| 		members: Object.entries(members).map(member => ({ | ||||
| 			id: member[0], | ||||
| 			...member[1] | ||||
| 		})) | ||||
| 	}; | ||||
| 	if (vybe.threads[thread.id]) | ||||
| 		Object.assign(thread, vybe.threads[thread.id]); | ||||
| 	else | ||||
| 		thread.streams = []; | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		thread | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function edit_thread(msg, respond) { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.id || typeof msg.name !== 'string') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'invalid msg' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (msg.name.length > 200) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'thread name 200 chars max' | ||||
| 		}); | ||||
| 	} | ||||
| 	const perms = await check_permission(msg.auth_user.id, msg.id); | ||||
| 	if (!perms.admin) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "user doesn't have permission" | ||||
| 		}); | ||||
| 	} | ||||
| 	// update name
 | ||||
| 	await db.query( | ||||
| 		'update thread set name = ? where id = ?', | ||||
| 		[msg.name, msg.id] | ||||
| 	); | ||||
| 	// update permissions
 | ||||
| 	let permissions = {}; | ||||
| 	for (const p of (await db.query( | ||||
| 		`select type, permission, value, mutable
 | ||||
| 		from permission | ||||
| 		where type != 'user' and thread = ?`,
 | ||||
| 		[msg.id] | ||||
| 	)).rows) { | ||||
| 		(permissions[p.type] || (permissions[p.type] = {})) | ||||
| 			[p.permission] = { | ||||
| 				value: p.value, | ||||
| 				mutable: p.mutable | ||||
| 			}; | ||||
| 	} | ||||
| 	async function setPermission(type, permission, value) { | ||||
| 		if (permissions[type] && permissions[type][permission]) { | ||||
| 			if (!permissions[type][permission].mutable) { | ||||
| 				respond({ | ||||
| 					success: false, | ||||
| 					message: 'permission not mutable' | ||||
| 				}); | ||||
| 				return false; | ||||
| 			} | ||||
| 			if (permissions[type][permission].value !== value) { | ||||
| 				await db.query(` | ||||
| 					update permission set value = ? | ||||
| 					where thread = ? and type = ? and permission = ?`,
 | ||||
| 					[value, msg.id, type, permission] | ||||
| 				); | ||||
| 				permissions[type][permission].value = value; | ||||
| 			} | ||||
| 		} | ||||
| 		else { | ||||
| 			await db.query( | ||||
| 				`insert into permission (thread, type, mutable, permission, value) 
 | ||||
| 				values (?, ?, ?, ?, ?)`,
 | ||||
| 				[msg.id, type, true, permission, value] | ||||
| 			); | ||||
| 			(permissions[type] || (permissions[type] = {})) | ||||
| 				[permission] = { | ||||
| 					value, | ||||
| 					mutable: true | ||||
| 				}; | ||||
| 		} | ||||
| 		return true; | ||||
| 	} | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		if (!await setPermission('everyone', 'view', 'true')) | ||||
| 			return; | ||||
| 		if (!msg.permissions || !msg.permissions.post_limited) { | ||||
| 			if (!await setPermission('everyone', 'post', 'true')) | ||||
| 				return; | ||||
| 		} | ||||
| 		else { | ||||
| 			if (!await setPermission('members', 'post', 'true')) | ||||
| 				return; | ||||
| 			if (!await setPermission('everyone', 'post', 'false')) | ||||
| 				return; | ||||
| 		} | ||||
| 	} else { | ||||
| 		if (!await setPermission('members', 'view', 'true')) | ||||
| 			return; | ||||
| 		if (!await setPermission('members', 'post', 'true')) | ||||
| 			return; | ||||
| 		if (!await setPermission('everyone', 'view', 'false')) | ||||
| 			return; | ||||
| 		if (!await setPermission('everyone', 'post', 'false')) | ||||
| 			return; | ||||
| 	} | ||||
| 	permissions.everyone.view.value = permissions.everyone.view.value === 'true'; | ||||
| 	permissions.everyone.post.value = permissions.everyone.post.value === 'true'; | ||||
| 	if (!permissions.members) permissions.members = {}; | ||||
| 	if (!permissions.members.view) | ||||
| 		permissions.members.view = { value: 'true', mutable: true }; | ||||
| 	if (!permissions.members.post) | ||||
| 		permissions.members.post = { value: 'true', mutable: true }; | ||||
| 	permissions.members.view.value = permissions.members.view.value === 'true'; | ||||
| 	permissions.members.post.value = permissions.members.post.value === 'true'; | ||||
| 	let members = Object.fromEntries((await db.query( | ||||
| 			`select user.name, user.id from thread
 | ||||
| 			join member on thread.id = member.thread | ||||
| 			join user on user.id = member.user | ||||
| 			where thread.id = ?`,
 | ||||
| 			[msg.id] | ||||
| 		)).rows.map(row => [row.name, row.id])); | ||||
| 	if (!msg.permissions || !msg.permissions.view_limited) { | ||||
| 		for (let username in vybe.users) | ||||
| 			for (let socket of vybe.users[username].sockets) | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: msg.id, | ||||
| 					permissions: { | ||||
| 						is_member: username in members, | ||||
| 						view: true, | ||||
| 						post: !msg.permissions || !msg.permissions.post_limited, | ||||
| 						...permissions | ||||
| 					} | ||||
| 				}); | ||||
| 	} | ||||
| 	else { | ||||
| 		for (let member in members) { | ||||
| 			if (!vybe.users[member]) | ||||
| 				continue; | ||||
| 			for (let socket of vybe.users[member].sockets) | ||||
| 				socket.emit('thread', { | ||||
| 					name: msg.name, | ||||
| 					id: msg.id, | ||||
| 					permissions: { | ||||
| 						is_member: true, | ||||
| 						view: true, | ||||
| 						post: true, | ||||
| 						admin: member.id === msg.auth_user.id && perms.admin, | ||||
| 						...permissions | ||||
| 					} | ||||
| 				}); | ||||
| 		} | ||||
| 	} | ||||
| 	return respond({ | ||||
| 		success: true | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	create_thread: authwrap(create_thread), | ||||
| 	list_threads: authwrap(list_threads), | ||||
| 	get_thread: authwrap(get_thread), | ||||
| 	edit_thread: authwrap(edit_thread) | ||||
| }; | ||||
|  | @ -1,7 +1,59 @@ | |||
| const db = require('../db'); | ||||
| const authwrap = require('../authwrap'); | ||||
| const openpgp = require('openpgp'); | ||||
| 
 | ||||
| const authenticate = async (msg, respond, socket) => { | ||||
| async function create_user(msg, respond) { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.name) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'username required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (!msg.pubkey) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'public key required' | ||||
| 		}); | ||||
| 	} | ||||
| 	// ensure username is not taken
 | ||||
| 	const result = await db.query('select * from user where name = ?', [ | ||||
| 		msg.name | ||||
| 	]); | ||||
| 	if (result.rows.length > 0) { | ||||
| 		console.log(`username already exists: ${result}`); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'a user with this name already exists on this server' | ||||
| 		}); | ||||
| 	} | ||||
| 	// validate public key
 | ||||
| 	try { | ||||
| 		await openpgp.readKey({ armoredKey: msg.pubkey }); | ||||
| 	} catch (err) { | ||||
| 		console.err('error in create_user readkey: ' + err); | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'public key invalid' | ||||
| 		}); | ||||
| 	} | ||||
| 	// add to db
 | ||||
| 	const insert = await db.query( | ||||
| 		'insert into user (name) values (?) returning id', | ||||
| 		[msg.name] | ||||
| 	); | ||||
| 	await db.query( | ||||
| 		'insert into key (user, pubkey, active) values (?, ?, true)', | ||||
| 		[insert.rows[0].id, msg.pubkey] | ||||
| 	) | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		id: insert.rows[0].id | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function authenticate(msg, respond, socket) { | ||||
| 	if (!msg.name || !msg.message) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
|  | @ -91,6 +143,36 @@ const authenticate = async (msg, respond, socket) => { | |||
| 			message: 'message signature verification failed' | ||||
| 		}); | ||||
| 	} | ||||
| }; | ||||
| } | ||||
| 
 | ||||
| module.exports = authenticate; | ||||
| async function get_keys(msg, respond) { | ||||
| 	// validate inputs
 | ||||
| 	if (!msg.names) { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: 'user names required' | ||||
| 		}); | ||||
| 	} | ||||
| 	if (typeof msg.names !== 'object') { | ||||
| 		return respond({ | ||||
| 			success: false, | ||||
| 			message: "can't iterate user names" | ||||
| 		}); | ||||
| 	} | ||||
| 	const keys = await db.query( // todo: encryption !
 | ||||
| 		`select name from user where name in
 | ||||
| 			(${msg.names.map((i) => '?').join(',')})`,
 | ||||
| 		msg.names | ||||
| 	); | ||||
| 	// respond
 | ||||
| 	return respond({ | ||||
| 		success: true, | ||||
| 		keys: keys.rows | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	create_user, | ||||
| 	authenticate, | ||||
| 	get_keys: authwrap(get_keys) | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue