diff --git a/client/app.js b/client/app.js
index f2dcd93..944fd16 100644
--- a/client/app.js
+++ b/client/app.js
@@ -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() {
-
{
if (event.key === 'Enter') {
@@ -267,7 +276,8 @@ document.body.append(html.node`
+ + ${stream.user}${stream.name ? ` - ${stream.name}` : ''} +
`; + 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` + + stream name: + `); + div.children['streamname'].oninput = event => { + if (handle) + window.emit('stream', { + id: streamid, + thread: window.currentThread.id, + name: div.children['streamname'].value + }); + }; + } + div.insertAdjacentHTML('beforeend', ` +streams:
+ `); + 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; diff --git a/index.js b/index.js index 7324e2c..2d3ea27 100644 --- a/index.js +++ b/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')); diff --git a/src/event/create_thread.js b/src/event/create_thread.js deleted file mode 100644 index e0b7396..0000000 --- a/src/event/create_thread.js +++ /dev/null @@ -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); diff --git a/src/event/create_user.js b/src/event/create_user.js deleted file mode 100644 index b4528fd..0000000 --- a/src/event/create_user.js +++ /dev/null @@ -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; diff --git a/src/event/edit_thread.js b/src/event/edit_thread.js deleted file mode 100644 index ed21cf4..0000000 --- a/src/event/edit_thread.js +++ /dev/null @@ -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); diff --git a/src/event/get_history.js b/src/event/get_history.js deleted file mode 100644 index 6813c90..0000000 --- a/src/event/get_history.js +++ /dev/null @@ -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); diff --git a/src/event/get_keys.js b/src/event/get_keys.js deleted file mode 100644 index 34b7526..0000000 --- a/src/event/get_keys.js +++ /dev/null @@ -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); diff --git a/src/event/get_space.js b/src/event/get_space.js deleted file mode 100644 index f92dd17..0000000 --- a/src/event/get_space.js +++ /dev/null @@ -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); diff --git a/src/event/get_thread.js b/src/event/get_thread.js deleted file mode 100644 index e72e446..0000000 --- a/src/event/get_thread.js +++ /dev/null @@ -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); diff --git a/src/event/list_threads.js b/src/event/list_threads.js deleted file mode 100644 index 60a50ec..0000000 --- a/src/event/list_threads.js +++ /dev/null @@ -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); diff --git a/src/event/send_message.js b/src/events/message.js similarity index 55% rename from src/event/send_message.js rename to src/events/message.js index 5e7d104..8f4a399 100644 --- a/src/event/send_message.js +++ b/src/events/message.js @@ -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) +}; diff --git a/src/event/save_span.js b/src/events/space.js similarity index 72% rename from src/event/save_span.js rename to src/events/space.js index e6fc332..9157cf0 100644 --- a/src/event/save_span.js +++ b/src/events/space.js @@ -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) +}; diff --git a/src/events/stream.js b/src/events/stream.js new file mode 100644 index 0000000..cdf3fb2 --- /dev/null +++ b/src/events/stream.js @@ -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) +}; diff --git a/src/events/thread.js b/src/events/thread.js new file mode 100644 index 0000000..117646e --- /dev/null +++ b/src/events/thread.js @@ -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) +}; diff --git a/src/event/authenticate.js b/src/events/user.js similarity index 58% rename from src/event/authenticate.js rename to src/events/user.js index 58ca527..fc430d6 100644 --- a/src/event/authenticate.js +++ b/src/events/user.js @@ -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) +};