audio streaming
parent
4416a3766b
commit
8ef0172d11
|
@ -1,6 +1,7 @@
|
||||||
import { render, html } from '/uhtml.js';
|
import { render, html } from '/uhtml.js';
|
||||||
import loadMessages from '/message.js';
|
import loadMessages from '/message.js';
|
||||||
import loadSpace from '/space.js';
|
import loadSpace from '/space.js';
|
||||||
|
import loadStreams from '/stream.js';
|
||||||
|
|
||||||
function setVisibility() {
|
function setVisibility() {
|
||||||
document.getElementById('visibility').innerText = `${
|
document.getElementById('visibility').innerText = `${
|
||||||
|
@ -33,6 +34,7 @@ function chooseThread() {
|
||||||
document.getElementById('threadname').textContent = this.thread.name;
|
document.getElementById('threadname').textContent = this.thread.name;
|
||||||
this.classList.add('active');
|
this.classList.add('active');
|
||||||
window.currentThread = this.thread;
|
window.currentThread = this.thread;
|
||||||
|
loadStreams();
|
||||||
if (this.tab)
|
if (this.tab)
|
||||||
switchTab(document.getElementById(this.tab));
|
switchTab(document.getElementById(this.tab));
|
||||||
else // load first tab that has any content
|
else // load first tab that has any content
|
||||||
|
@ -40,8 +42,16 @@ function chooseThread() {
|
||||||
if (messages.length)
|
if (messages.length)
|
||||||
switchTab(document.getElementById(this.tab = 'messagetab'));
|
switchTab(document.getElementById(this.tab = 'messagetab'));
|
||||||
else
|
else
|
||||||
loadSpace(spans => switchTab(
|
loadSpace(spans => {
|
||||||
document.getElementById(this.tab = spans.length ? 'spacetab' : 'messagetab')));
|
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.emit('get_thread', { thread: this.thread.id }, msg => {
|
||||||
window.currentThread = msg.thread;
|
window.currentThread = msg.thread;
|
||||||
|
@ -148,7 +158,6 @@ function newThread() {
|
||||||
<input type='radio' name='newpermissions'
|
<input type='radio' name='newpermissions'
|
||||||
id='private_view' value='private_view' />
|
id='private_view' value='private_view' />
|
||||||
<label for='private_view'>only members can view and post</label><br>
|
<label for='private_view'>only members can view and post</label><br>
|
||||||
<br>
|
|
||||||
<label class='heading' for='membername'>members</label>
|
<label class='heading' for='membername'>members</label>
|
||||||
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
|
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
|
@ -267,7 +276,8 @@ document.body.append(html.node`
|
||||||
<div id='buttonbar'>
|
<div id='buttonbar'>
|
||||||
<div id='tabs'>
|
<div id='tabs'>
|
||||||
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages
|
<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>
|
</div>
|
||||||
<button id='showmembers' onclick=${() =>
|
<button id='showmembers' onclick=${() =>
|
||||||
document.getElementById('members').classList.toggle('hidden')
|
document.getElementById('members').classList.toggle('hidden')
|
||||||
|
@ -275,6 +285,7 @@ document.body.append(html.node`
|
||||||
</div>
|
</div>
|
||||||
<div id='message' class='tabcontent'></div>
|
<div id='message' class='tabcontent'></div>
|
||||||
<div id='space' class='tabcontent hidden'></div>
|
<div id='space' class='tabcontent hidden'></div>
|
||||||
|
<div id='stream' class='tabcontent hidden'></div>
|
||||||
</div>
|
</div>
|
||||||
<hr class='separator' color='#505050'>
|
<hr class='separator' color='#505050'>
|
||||||
<div id='members' class='hidden'>
|
<div id='members' class='hidden'>
|
||||||
|
|
|
@ -81,8 +81,11 @@
|
||||||
background-color: #4f4f4f;
|
background-color: #4f4f4f;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
|
margin: 5px 1px;
|
||||||
|
}
|
||||||
label.heading {
|
label.heading {
|
||||||
margin-bottom: 5px;
|
margin: 10px 1px 4px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
h3 {
|
h3 {
|
||||||
|
@ -218,9 +221,6 @@
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.member {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
#space {
|
#space {
|
||||||
margin: -2px; /* offset column margin */
|
margin: -2px; /* offset column margin */
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -230,6 +230,13 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
#stream {
|
||||||
|
margin: 4px 2px;
|
||||||
|
}
|
||||||
|
#play {
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: 'Segoe UI Symbol', math;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body></body>
|
<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;
|
41
index.js
41
index.js
|
@ -4,10 +4,12 @@ const http = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
|
|
||||||
const events = Object.fromEntries(
|
const events = {};
|
||||||
fs.readdirSync('./src/event')
|
for (let file of fs.readdirSync('./src/events')) {
|
||||||
.map(event => [event.slice(0, -3), require('./src/event/' + event)])
|
file = require('./src/events/' + file);
|
||||||
);
|
for (const event in file)
|
||||||
|
events[event] = file[event];
|
||||||
|
}
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3435;
|
const PORT = process.env.PORT || 3435;
|
||||||
|
|
||||||
|
@ -16,10 +18,18 @@ app.use(compression());
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
cors: {
|
cors: {
|
||||||
origin: true,
|
origin: true
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(express.static('client'));
|
||||||
|
|
||||||
|
global.vybe = {
|
||||||
|
users: {},
|
||||||
|
threads: {},
|
||||||
|
streams: {}
|
||||||
|
};
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
for (let event in events) {
|
for (let event in events) {
|
||||||
socket.on(event, (msg, callback) => {
|
socket.on(event, (msg, callback) => {
|
||||||
|
@ -27,22 +37,27 @@ io.on('connection', (socket) => {
|
||||||
callback('no such event ' + event);
|
callback('no such event ' + event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
events[event](msg, callback, socket);
|
events[event](msg, callback, socket);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`${event} threw exception: `, e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
socket.on('disconnect', reason => {
|
socket.on('disconnect', reason => {
|
||||||
let user = vybe.users[socket.username];
|
let user = vybe.users[socket.username];
|
||||||
if (user)
|
if (user)
|
||||||
user.sockets.splice(user.sockets.indexOf(socket), 1);
|
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, () => {
|
server.listen(PORT, () => {
|
||||||
console.log('server running on port ' + 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 authwrap = require('../authwrap');
|
||||||
const check_permission = require('../check_permission');
|
const check_permission = require('../check_permission');
|
||||||
|
|
||||||
const send_message = async (msg, respond) => {
|
async function send_message(msg, respond) {
|
||||||
if (!msg.thread) {
|
if (!msg.thread) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -30,8 +30,7 @@ const send_message = async (msg, respond) => {
|
||||||
// get perms
|
// get perms
|
||||||
const permissions = await db.query(
|
const permissions = await db.query(
|
||||||
"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
|
"select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'",
|
||||||
[msg.thread]
|
[msg.thread]);
|
||||||
);
|
|
||||||
for (let username in vybe.users) {
|
for (let username in vybe.users) {
|
||||||
if (permissions.rows.length > 0 || members.includes(username)) {
|
if (permissions.rows.length > 0 || members.includes(username)) {
|
||||||
for (let s of vybe.users[username].sockets) {
|
for (let s of vybe.users[username].sockets) {
|
||||||
|
@ -48,6 +47,46 @@ const send_message = async (msg, respond) => {
|
||||||
success: true,
|
success: true,
|
||||||
id: id.rows[0].id
|
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 authwrap = require('../authwrap');
|
||||||
const check_permission = require('../check_permission');
|
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) {
|
if (!msg.thread) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -62,6 +85,9 @@ const save_span = async (msg, respond, socket) => {
|
||||||
success: true,
|
success: true,
|
||||||
id
|
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 db = require('../db');
|
||||||
|
const authwrap = require('../authwrap');
|
||||||
const openpgp = require('openpgp');
|
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) {
|
if (!msg.name || !msg.message) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -91,6 +143,36 @@ const authenticate = async (msg, respond, socket) => {
|
||||||
message: 'message signature verification failed'
|
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