federation !
parent
f34099de9c
commit
02294b0327
41
README.md
41
README.md
|
@ -1,21 +1,40 @@
|
||||||
# vybe server
|
|
||||||
|
|
||||||
very very unfinished !! right now it can do a chatroom
|
## vybe
|
||||||
|
|
||||||
## usage
|
vybe is a work-in-progress decentralized **communication network**.
|
||||||
|
|
||||||
|
a vybe instance (server) features user accounts and **threads**.
|
||||||
|
|
||||||
|
user accounts are owned by PGP keys stored in users' clients.
|
||||||
|
|
||||||
|
each thread features a chatroom, a **space**, and audio streams.
|
||||||
|
|
||||||
|
a space is like a text blackboard, basically. you can write text and move it around and resize it with your mouse.
|
||||||
|
|
||||||
|
once you have an account on any instance, you can connect to and interact with any other instance, right from within your home instance ui.
|
||||||
|
|
||||||
|
to run your own instance, clone this repo and:
|
||||||
|
|
||||||
|
> `npm install`
|
||||||
|
|
||||||
`npm install`
|
|
||||||
then
|
then
|
||||||
`node index.js`
|
|
||||||
|
|
||||||
## clients
|
> `node .`
|
||||||
|
|
||||||
there are two clients! you can choose one
|
it will run on port 1312 by default; you can change this by setting PORT on first-run:
|
||||||
|
|
||||||
### vybeclient
|
> `PORT=1234 node .`
|
||||||
|
|
||||||
standard official client, written w/ vanilla js. served on :3435 by index.js
|
after first-run, the port is configured in instance.json.
|
||||||
|
|
||||||
### vyber
|
currently, you'll need to proxy it through a webserver like nginx or Caddy, which needs to have https enabled for things to work correctly. if you want the easy option, i recommend Caddy. example Caddy config:
|
||||||
|
|
||||||
alternative client, developed by june, w/ webcomponents. start by running `npm start` in the vyber-client folder
|
> ```
|
||||||
|
> vybe.my.domain {
|
||||||
|
> reverse_proxy localhost:1312
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
then go to `https://vybe.my.domain` to start using vybe!
|
||||||
|
|
||||||
|
let me know if you have any questions or issues. my website is [jerl.zone](https://jerl.zone).
|
||||||
|
|
466
client/app.js
466
client/app.js
|
@ -1,254 +1,5 @@
|
||||||
import { render, html } from '/uhtml.js';
|
import { render, html } from '/uhtml.js';
|
||||||
import loadMessages from '/message.js';
|
import loadThreads from '/thread.js';
|
||||||
import loadSpace from '/space.js';
|
|
||||||
import loadStreams from '/stream.js';
|
|
||||||
|
|
||||||
function setVisibility() {
|
|
||||||
document.getElementById('visibility').innerText = `${
|
|
||||||
window.currentThread.permissions.everyone.view.value ?
|
|
||||||
'this thread is visible to everyone' :
|
|
||||||
'members can view this thread'}
|
|
||||||
${window.currentThread.permissions.everyone.post.value ?
|
|
||||||
'anyone can post' :
|
|
||||||
window.currentThread.permissions.members.post.value ?
|
|
||||||
'only members can post' : 'some members can post'}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function chooseThread() {
|
|
||||||
const edit = document.getElementById('edit');
|
|
||||||
if (window.currentThread) {
|
|
||||||
document.getElementById(`thread${window.currentThread.id}`).classList.remove('active');
|
|
||||||
let editform = document.getElementById('editthread');
|
|
||||||
if (editform) {
|
|
||||||
editform.remove();
|
|
||||||
edit.textContent = 'edit';
|
|
||||||
}
|
|
||||||
if (window.currentThread.id === this.thread.id) {
|
|
||||||
document.getElementById('thread').classList.add('hidden');
|
|
||||||
window.currentThread = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
document.getElementById('thread').classList.remove('hidden');
|
|
||||||
if (this.thread.permissions.admin)
|
|
||||||
edit.classList.remove('hidden');
|
|
||||||
else
|
|
||||||
edit.classList.add('hidden');
|
|
||||||
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
|
|
||||||
loadMessages(true, messages => {
|
|
||||||
if (messages.length)
|
|
||||||
switchTab(document.getElementById(this.tab = 'messagetab'));
|
|
||||||
else
|
|
||||||
loadSpace(spans => {
|
|
||||||
if (spans.length)
|
|
||||||
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;
|
|
||||||
setVisibility();
|
|
||||||
document.getElementById('memberlist').replaceChildren(
|
|
||||||
...msg.thread.members.map(member =>
|
|
||||||
html.node`<p class='member'>${member.name}</p>`)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchTab(tab) {
|
|
||||||
for (let tab of document.querySelectorAll('.tab'))
|
|
||||||
tab.classList.remove('active');
|
|
||||||
for (let tab of document.querySelectorAll('.tabcontent'))
|
|
||||||
tab.classList.add('hidden');
|
|
||||||
tab.classList.add('active');
|
|
||||||
document
|
|
||||||
.getElementById(tab.id.slice(0, -3))
|
|
||||||
.classList.remove('hidden');
|
|
||||||
if (tab.id === 'messagetab')
|
|
||||||
loadMessages(true);
|
|
||||||
else if (tab.id === 'spacetab')
|
|
||||||
loadSpace();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createThread(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
let name = document.getElementById('newthreadname').value;
|
|
||||||
if (!name) {
|
|
||||||
document.getElementById('newnameempty').classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const perms = document.querySelector(
|
|
||||||
'input[name="newpermissions"]:checked'
|
|
||||||
).value;
|
|
||||||
let permissions;
|
|
||||||
if (perms === 'public')
|
|
||||||
permissions = { view_limited: false, post_limited: false };
|
|
||||||
else if (perms === 'private_post')
|
|
||||||
permissions = { view_limited: false, post_limited: true };
|
|
||||||
else if (perms === 'private_view') {
|
|
||||||
permissions = { view_limited: true, post_limited: true };
|
|
||||||
// generate key
|
|
||||||
/* wip
|
|
||||||
var buf = new Uint8Array(32);
|
|
||||||
crypto.getRandomValues(buf);
|
|
||||||
const key = aesjs.utils.hex.fromBytes(Array.from(buf));
|
|
||||||
// sign it to each of the members
|
|
||||||
for (let i = 0; i < newmembers.length; i++) {
|
|
||||||
const member = newmembers[i];
|
|
||||||
const sig = await openpgp.encrypt({
|
|
||||||
message: await openpgp.createMessage({ text: key }),
|
|
||||||
signingKeys: window.keys.priv
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
window.emit('create_thread',
|
|
||||||
{ name, permissions, members: window.threadmembers },
|
|
||||||
msg => {
|
|
||||||
chooseThread.call(document.getElementById('thread' + msg.id));
|
|
||||||
// since the form exists, this will perform cleanup
|
|
||||||
newThread();
|
|
||||||
document.getElementById('loadmore').classList.add('hidden');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addMember() {
|
|
||||||
const name = document.getElementById('membername');
|
|
||||||
if (!name.value)
|
|
||||||
return;
|
|
||||||
window.threadmembers.push({ name: name.value });
|
|
||||||
document.getElementById('newmembers')
|
|
||||||
.appendChild(html.node`<p class='member'>${name.value}</p>`);
|
|
||||||
name.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function newThread() {
|
|
||||||
let form = document.getElementById('createthread');
|
|
||||||
if (form) {
|
|
||||||
form.remove();
|
|
||||||
document.getElementById('newthread').textContent = 'create';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.threadmembers = [{
|
|
||||||
name: window.name,
|
|
||||||
permissions: { admin: 'true' }
|
|
||||||
}];
|
|
||||||
document.querySelector('#home + .separator').insertAdjacentElement('afterend', html.node`
|
|
||||||
<form id='createthread' class='column' onsubmit=${createThread}>
|
|
||||||
<h3>create thread</h3>
|
|
||||||
<label for='newthreadname' class='heading'>thread name</label>
|
|
||||||
<p id='newnameempty' class='hidden'>name cannot be empty</p>
|
|
||||||
<input type='text' id='newthreadname' />
|
|
||||||
<p id='permissions'>thread permissions</p>
|
|
||||||
<input type='radio' name='newpermissions'
|
|
||||||
id='public' value='public' checked />
|
|
||||||
<label for='public'>anyone can view and post</label><br>
|
|
||||||
<input type='radio' name='newpermissions'
|
|
||||||
id='private_post' value='private_post' />
|
|
||||||
<label for='private_post'>anyone can view, only members can post</label><br>
|
|
||||||
<input type='radio' name='newpermissions'
|
|
||||||
id='private_view' value='private_view' />
|
|
||||||
<label for='private_view'>only members can view and post</label><br>
|
|
||||||
<label class='heading' for='membername'>members</label>
|
|
||||||
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
addMember();
|
|
||||||
}
|
|
||||||
}} />
|
|
||||||
<button id='addmember' type='button' onclick=${addMember}>add</button>
|
|
||||||
<div id='newmembers'>
|
|
||||||
<p class='member'>${window.name}</p>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<button id='submitthread' type='submit'>create</button>
|
|
||||||
</form>`);
|
|
||||||
document.getElementById('newthread').textContent = 'cancel';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveThread(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
let name = document.getElementById('editthreadname').value;
|
|
||||||
if (!name) {
|
|
||||||
document.getElementById('nameempty').classList.remove('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const perms = document.querySelector(
|
|
||||||
'input[name="permissions"]:checked'
|
|
||||||
).value;
|
|
||||||
let permissions;
|
|
||||||
if (perms === 'public')
|
|
||||||
permissions = { view_limited: false, post_limited: false };
|
|
||||||
else if (perms === 'private_post')
|
|
||||||
permissions = { view_limited: false, post_limited: true };
|
|
||||||
else if (perms === 'private_view') {
|
|
||||||
permissions = { view_limited: true, post_limited: true };
|
|
||||||
// todo: generate key and encrypt
|
|
||||||
}
|
|
||||||
window.emit('edit_thread',
|
|
||||||
{ id: window.currentThread.id, name, permissions },
|
|
||||||
msg => {
|
|
||||||
if (!msg.success) {
|
|
||||||
console.log('edit_thread failed: ', msg.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editThread();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function editThread() {
|
|
||||||
let form = document.getElementById('editthread');
|
|
||||||
if (form) {
|
|
||||||
form.remove();
|
|
||||||
document.getElementById('edit').textContent = 'edit';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
form = html.node`
|
|
||||||
<form id='editthread' class='column' onsubmit=${saveThread}>
|
|
||||||
<label for='editthreadname' class='heading'>thread name</label>
|
|
||||||
<input type='text' id='editthreadname' />
|
|
||||||
<p id='nameempty' class='hidden'>name cannot be empty</p>
|
|
||||||
<p id='permissions'>thread permissions</p>
|
|
||||||
<input type='radio' name='permissions'
|
|
||||||
id='public' value='public' />
|
|
||||||
<label for='public'>anyone can view and post</label><br>
|
|
||||||
<input type='radio' name='permissions'
|
|
||||||
id='private_post' value='private_post' />
|
|
||||||
<label for='private_post'>anyone can view, only members can post</label><br>
|
|
||||||
<input type='radio' name='permissions'
|
|
||||||
id='private_view' value='private_view' />
|
|
||||||
<label for='private_view'>only members can view and post</label><br>
|
|
||||||
<br>
|
|
||||||
<button id='savethread'>save</button>
|
|
||||||
</form>`;
|
|
||||||
form['editthreadname'].value = window.currentThread.name;
|
|
||||||
if (window.currentThread.permissions.everyone.post.value)
|
|
||||||
form['public'].checked = true;
|
|
||||||
else if (window.currentThread.permissions.everyone.view.value)
|
|
||||||
form['private_post'].checked = true;
|
|
||||||
else
|
|
||||||
form['private_view'].checked = true;
|
|
||||||
document.querySelector('#thread').append(form);
|
|
||||||
document.getElementById('edit').textContent = 'cancel';
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickedTab(event) {
|
|
||||||
switchTab(event.target);
|
|
||||||
document.getElementById(`thread${window.currentThread.id}`).tab = event.target.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeName(e) {
|
function changeName(e) {
|
||||||
let displayname = document.getElementById('newname').value;
|
let displayname = document.getElementById('newname').value;
|
||||||
|
@ -265,14 +16,140 @@ function changeName(e) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.connectInstance = async url => {
|
||||||
|
let instance = window.instances[url];
|
||||||
|
let connected;
|
||||||
|
function connecting(resolve, reject) {
|
||||||
|
instance.socket.on('connect', resolve);
|
||||||
|
instance.socket.on('connect_error', error => {
|
||||||
|
console.log(`error connecting to ${url}: ${error}`);
|
||||||
|
if (connected)
|
||||||
|
return;
|
||||||
|
instance.socket.disconnect();
|
||||||
|
reject(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (instance) {
|
||||||
|
if (!instance.socket?.connected) {
|
||||||
|
if (instance.socket)
|
||||||
|
instance.socket.connect();
|
||||||
|
else
|
||||||
|
instance.socket = io(url);
|
||||||
|
await new Promise(connecting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
instance = window.instances[url] = {
|
||||||
|
socket: io(url),
|
||||||
|
url
|
||||||
|
};
|
||||||
|
await new Promise(connecting);
|
||||||
|
}
|
||||||
|
connected = true;
|
||||||
|
return instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.getUser = async (id, name) => {
|
||||||
|
let instance = window.instancelist[0];
|
||||||
|
if (id) {
|
||||||
|
id = id.split('@');
|
||||||
|
instance = await connectInstance(id[1]);
|
||||||
|
id = id[0];
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
instance.socket.emit('get_user', { id, name }, msg => {
|
||||||
|
if (!msg.success)
|
||||||
|
return reject('get_user failed: ' + msg.message);
|
||||||
|
resolve(msg.user);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function authenticateInstance(div, select) {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
div.instance.socket.emit('authenticate', {
|
||||||
|
name: window.name,
|
||||||
|
id: window.id + '@' + location.host,
|
||||||
|
message: window.signedMessage,
|
||||||
|
pubkey: window.keys.armored.publicKey
|
||||||
|
}, msg => {
|
||||||
|
if (!msg.success) {
|
||||||
|
reject(`${div.instance.url} authentication failed: ${msg.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
div.instance.id = msg.instance.id;
|
||||||
|
div.id = 'instance' + msg.instance.id;
|
||||||
|
div.instance.emit = (event, data, callback) =>
|
||||||
|
div.instance.socket.emit(event, {
|
||||||
|
...data,
|
||||||
|
__session: window.session,
|
||||||
|
}, callback);
|
||||||
|
loadThreads(div, select);
|
||||||
|
resolve();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandInstance(event) {
|
||||||
|
this.parentElement.parentElement.children['threads'].classList.toggle('hidden');
|
||||||
|
this.children[0].classList.toggle('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
function instanceClicked(event) {}
|
||||||
|
|
||||||
|
function saveInstances() {
|
||||||
|
localStorage.setItem('instances', JSON.stringify(
|
||||||
|
window.instancelist.map(i => ({
|
||||||
|
id: i.id,
|
||||||
|
url: i.url
|
||||||
|
}))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addInstance() {
|
||||||
|
this.textContent = this.textContent.trim().toLowerCase();
|
||||||
|
let instancediv = this.parentElement.parentElement;
|
||||||
|
if (!this.textContent)
|
||||||
|
return instancediv.remove();
|
||||||
|
let url = (/^[\w-]+:.+/.test(this.textContent) ? '' : 'https://') + this.textContent;
|
||||||
|
if (window.instancelist.find(i => i.url === url))
|
||||||
|
return instancediv.remove();
|
||||||
|
let instance = await connectInstance(url);
|
||||||
|
instancediv.instance = instance;
|
||||||
|
await authenticateInstance(instancediv, true);
|
||||||
|
this.contentEditable = false;
|
||||||
|
/* expander is initially just a span with a non-space space
|
||||||
|
in it to make the title span style work */
|
||||||
|
let expander = this.parentElement.children[0];
|
||||||
|
expander.classList.add('expander');
|
||||||
|
expander.onclick = expandInstance;
|
||||||
|
expander.innerHTML = `<span class='arrow'>◿</span>`;
|
||||||
|
window.instancelist.push(instance);
|
||||||
|
saveInstances();
|
||||||
|
this.onclick = instanceClicked;
|
||||||
|
}
|
||||||
|
|
||||||
// main app html
|
// main app html
|
||||||
document.body.append(html.node`
|
document.body.append(html.node`
|
||||||
<div id='home' class='column'>
|
<div id='home' class='column'>
|
||||||
<div id='threads'>
|
<div id='instancelist'>
|
||||||
<h3>vybe</h3>
|
<h3>vybe</h3>
|
||||||
<h4>threads</h4>
|
<p id='instances'>instances:<button onclick=${() => {
|
||||||
<div id='threadlist'>loading...</div>
|
let div = html.node`
|
||||||
<button id='newthread' onclick=${newThread}>create</button>
|
<div>
|
||||||
|
<div class='instance'>
|
||||||
|
<span> </span>
|
||||||
|
<span onblur=${addInstance} onkeydown=${function(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
addInstance.call(this);
|
||||||
|
}
|
||||||
|
}} class='title' contenteditable='true'>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.getElementById('instancelist').append(div);
|
||||||
|
div.children[0].children[1].focus();
|
||||||
|
}}>add</button></p>
|
||||||
</div>
|
</div>
|
||||||
<div id='user' onclick=${() =>
|
<div id='user' onclick=${() =>
|
||||||
document.getElementById('profile').classList.toggle('hidden')
|
document.getElementById('profile').classList.toggle('hidden')
|
||||||
|
@ -282,6 +159,7 @@ document.body.append(html.node`
|
||||||
<!-- create thread column goes here -->
|
<!-- create thread column goes here -->
|
||||||
<hr class='separator' color='#505050'>
|
<hr class='separator' color='#505050'>
|
||||||
<div id='profile' class='column hidden'>
|
<div id='profile' class='column hidden'>
|
||||||
|
<p><strong>profile</strong></p>
|
||||||
<label class='heading'>display name</label>
|
<label class='heading'>display name</label>
|
||||||
<input id='newname' onkeyup=${function(event) {
|
<input id='newname' onkeyup=${function(event) {
|
||||||
if (window.displayname === this.value)
|
if (window.displayname === this.value)
|
||||||
|
@ -296,63 +174,31 @@ document.body.append(html.node`
|
||||||
<div id='authrequests'></div>
|
<div id='authrequests'></div>
|
||||||
</div>
|
</div>
|
||||||
<hr class='separator' color='#505050'>
|
<hr class='separator' color='#505050'>
|
||||||
<div id='thread' class='column'>
|
|
||||||
<div id='content'>
|
|
||||||
<div id='titlebar'>
|
|
||||||
<span id='title'>thread: <strong id='threadname'>meow</strong></span>
|
|
||||||
<button id='edit' class='hidden' onclick=${editThread}>edit</button>
|
|
||||||
</div>
|
|
||||||
<div id='buttonbar'>
|
|
||||||
<div id='tabs'>
|
|
||||||
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages
|
|
||||||
</button><button id='spacetab' class='tab' onclick=${clickedTab}>space
|
|
||||||
</button><button id='streamtab' class='tab' onclick=${clickedTab}>streams</button>
|
|
||||||
</div>
|
|
||||||
<button id='showmembers' onclick=${() =>
|
|
||||||
document.getElementById('members').classList.toggle('hidden')
|
|
||||||
}>members</button>
|
|
||||||
</div>
|
|
||||||
<div id='message' class='tabcontent'></div>
|
|
||||||
<div id='space' class='tabcontent hidden'></div>
|
|
||||||
<div id='stream' class='tabcontent hidden'></div>
|
|
||||||
</div>
|
|
||||||
<hr class='separator' color='#505050'>
|
|
||||||
<div id='members' class='hidden'>
|
|
||||||
<p id='visibility'></p>
|
|
||||||
<p><strong>members</strong></p>
|
|
||||||
<div id='memberlist'>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr class='separator' color='#505050'>
|
|
||||||
</div>
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
document.getElementById('newname').value = window.displayname;
|
for (let i = 0; i < instancelist.length; ++i) {
|
||||||
|
let instance = instancelist[i];
|
||||||
function makeThread(thread) {
|
let div = html.node`
|
||||||
let node = html.node`
|
<div>
|
||||||
<div class='thread' onclick=${chooseThread}>${
|
<div class='instance'>
|
||||||
thread.name
|
<span class='expander' onclick=${expandInstance}>
|
||||||
}</div>`;
|
<span class='arrow'>◿</span>
|
||||||
node.id = 'thread' + thread.id;
|
</span>
|
||||||
node.thread = thread;
|
<span class='title' onclick=${instanceClicked}>${instance.url}</span>
|
||||||
return node;
|
</div>
|
||||||
|
</div>`;
|
||||||
|
div.id = 'instance' + instance.id;
|
||||||
|
div.instance = instance;
|
||||||
|
document.getElementById('instancelist').append(div);
|
||||||
|
if (i === 0)
|
||||||
|
loadThreads(div, true);
|
||||||
|
else {
|
||||||
|
await connectInstance(instance.url);
|
||||||
|
authenticateInstance(div);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.socket.on('thread', thread => {
|
document.getElementById('newname').value = window.displayname;
|
||||||
let el = document.getElementById('thread' + thread.id);
|
|
||||||
if (el) {
|
|
||||||
el.thread = thread;
|
|
||||||
el.textContent = thread.name;
|
|
||||||
if (window.currentThread?.id === thread.id) {
|
|
||||||
Object.assign(window.currentThread, thread);
|
|
||||||
document.getElementById('threadname').textContent = thread.name;
|
|
||||||
setVisibility();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById('threadlist').prepend(makeThread(thread));
|
|
||||||
});
|
|
||||||
|
|
||||||
function authRequest(authrequest) {
|
function authRequest(authrequest) {
|
||||||
const p = html.node`
|
const p = html.node`
|
||||||
|
@ -374,12 +220,6 @@ function authRequest(authrequest) {
|
||||||
|
|
||||||
window.socket.on('authrequest', authRequest);
|
window.socket.on('authrequest', authRequest);
|
||||||
|
|
||||||
window.emit('list_threads', {}, msg => {
|
|
||||||
const threadlist = document.getElementById('threadlist');
|
|
||||||
threadlist.replaceChildren(...msg.threads.map(makeThread));
|
|
||||||
chooseThread.call(threadlist.firstChild);
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
authRequest
|
authRequest
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,33 +12,59 @@ function rand() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function auth() {
|
async function auth() {
|
||||||
const sig = await openpgp.sign({
|
window.session = rand();
|
||||||
|
|
||||||
|
window.emit = (event, data, callback) =>
|
||||||
|
window.socket.emit(event, {
|
||||||
|
...data,
|
||||||
|
__session: window.session,
|
||||||
|
}, callback);
|
||||||
|
|
||||||
|
window.signedMessage = await openpgp.sign({
|
||||||
message: new openpgp.CleartextMessage('vybe_auth ' + window.session, ''),
|
message: new openpgp.CleartextMessage('vybe_auth ' + window.session, ''),
|
||||||
signingKeys: window.keys.priv
|
signingKeys: window.keys.priv
|
||||||
});
|
});
|
||||||
window.socket.emit(
|
|
||||||
'authenticate',
|
window.socket.emit('authenticate', {
|
||||||
{
|
name: window.name,
|
||||||
name: window.name,
|
id: `${window.id || ''}@${location.host}`,
|
||||||
message: sig,
|
message: signedMessage,
|
||||||
pubkey: window.keys.armored.publicKey
|
pubkey: window.keys.armored.publicKey
|
||||||
},
|
},
|
||||||
async msg => {
|
async msg => {
|
||||||
let register = document.getElementById('register');
|
let register = document.getElementById('register');
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
console.log('authenticate failed', msg);
|
console.log('authenticate failed:', msg.message);
|
||||||
document.getElementById('result').innerText = msg.message;
|
document.getElementById('result').innerText = msg.message;
|
||||||
register.classList.remove('hidden');
|
register.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('keys', JSON.stringify(window.keys.armored));
|
||||||
|
localStorage.setItem('name', window.name = msg.name);
|
||||||
|
localStorage.setItem('id', window.id = msg.id);
|
||||||
|
register.classList.add('hidden');
|
||||||
|
window.displayname = msg.displayname;
|
||||||
|
if (window.instancelist) {
|
||||||
|
if (window.instancelist[0].url !== location.host
|
||||||
|
|| window.instancelist[0].id !== msg.instance.id) {
|
||||||
|
console.log('instance url or id changed ??');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localStorage.setItem('keys', JSON.stringify(window.keys.armored));
|
|
||||||
localStorage.setItem('name', window.name);
|
|
||||||
register.classList.add('hidden');
|
|
||||||
window.displayname = msg.displayname;
|
|
||||||
const { authRequest } = await import('/app.js');
|
|
||||||
msg.authrequests.forEach(authRequest);
|
|
||||||
}
|
}
|
||||||
);
|
else {
|
||||||
|
localStorage.setItem('instances', JSON.stringify(window.instancelist = [
|
||||||
|
{
|
||||||
|
id: msg.instance.id,
|
||||||
|
url: location.host
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
instancelist[0].socket = window.socket;
|
||||||
|
instancelist[0].emit = window.emit;
|
||||||
|
window.instances = Object.fromEntries(instancelist.map(i => [i.url, i]));
|
||||||
|
const { authRequest } = await import('/app.js');
|
||||||
|
msg.authrequests.forEach(authRequest);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(event) {
|
async function submit(event) {
|
||||||
|
@ -54,7 +80,7 @@ async function submit(event) {
|
||||||
window.keys = { priv, pub, armored: keys };
|
window.keys = { priv, pub, armored: keys };
|
||||||
window.name = name;
|
window.name = name;
|
||||||
if (this.id === 'registerform') {
|
if (this.id === 'registerform') {
|
||||||
window.emit('create_user',
|
window.socket.emit('create_user',
|
||||||
{ name, pubkey: keys.publicKey },
|
{ name, pubkey: keys.publicKey },
|
||||||
(msg) => {
|
(msg) => {
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
|
@ -96,28 +122,14 @@ render(document.body, html`
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function gensession() {
|
|
||||||
window.session = rand();
|
|
||||||
|
|
||||||
window.emit = (type, data, callback) =>
|
|
||||||
window.socket.emit(
|
|
||||||
type,
|
|
||||||
{
|
|
||||||
...data,
|
|
||||||
__session: window.session,
|
|
||||||
},
|
|
||||||
callback
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onload = async () => {
|
window.onload = async () => {
|
||||||
window.socket = io();
|
window.socket = io();
|
||||||
|
|
||||||
gensession();
|
|
||||||
|
|
||||||
let keys = localStorage.getItem('keys');
|
let keys = localStorage.getItem('keys');
|
||||||
if (keys) {
|
if (keys) {
|
||||||
window.name = localStorage.getItem('name');
|
window.name = localStorage.getItem('name');
|
||||||
|
window.id = localStorage.getItem('id');
|
||||||
|
window.instancelist = JSON.parse(localStorage.getItem('instances'));
|
||||||
keys = JSON.parse(keys);
|
keys = JSON.parse(keys);
|
||||||
window.keys = {
|
window.keys = {
|
||||||
priv: await openpgp.readKey({ armoredKey: keys.privateKey }),
|
priv: await openpgp.readKey({ armoredKey: keys.privateKey }),
|
||||||
|
@ -130,7 +142,6 @@ window.onload = async () => {
|
||||||
document.getElementById('register').classList.remove('hidden');
|
document.getElementById('register').classList.remove('hidden');
|
||||||
|
|
||||||
window.socket.io.on('reconnect', async attempt => {
|
window.socket.io.on('reconnect', async attempt => {
|
||||||
gensession();
|
|
||||||
if (window.keys)
|
if (window.keys)
|
||||||
await auth();
|
await auth();
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
&:focus {
|
&:focus {
|
||||||
padding-bottom: 3px;
|
padding-bottom: 2px;
|
||||||
border-bottom: 3px solid #777;
|
border-bottom: 3px solid #777;
|
||||||
}
|
}
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
|
@ -70,20 +70,21 @@
|
||||||
margin-inline: 14px;
|
margin-inline: 14px;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
:not(#register) > p {
|
||||||
|
margin: 5px 1px;
|
||||||
|
}
|
||||||
.thread:hover,
|
.thread:hover,
|
||||||
.tab:hover,
|
.tab:hover,
|
||||||
|
.instance > span:hover,
|
||||||
#user:hover {
|
#user:hover {
|
||||||
background-color: #303030;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
.tab.active,
|
|
||||||
.thread.active,
|
.thread.active,
|
||||||
|
.tab.active,
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #4f4f4f;
|
background-color: #4f4f4f;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
p {
|
|
||||||
margin: 5px 1px;
|
|
||||||
}
|
|
||||||
label.heading {
|
label.heading {
|
||||||
margin: 10px 1px 4px;
|
margin: 10px 1px 4px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -109,13 +110,47 @@
|
||||||
.separator:last-child {
|
.separator:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.instance {
|
||||||
|
margin: 2px;
|
||||||
|
> span {
|
||||||
|
display: table-cell;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
> .title {
|
||||||
|
padding-block: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.expander {
|
||||||
|
padding-inline: 6px;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
rotate: 45deg;
|
||||||
|
&.collapsed {
|
||||||
|
rotate: -45deg;
|
||||||
|
top: 0;
|
||||||
|
left: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
#home {
|
#home {
|
||||||
|
margin: 0;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
#instancelist > :not(div) {
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
#instances {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
#threads {
|
#threads {
|
||||||
|
margin: 0 2px 6px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -124,8 +159,9 @@
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
#user {
|
#user {
|
||||||
|
margin: 2px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
background-color: #191919;
|
background-color: #222;
|
||||||
}
|
}
|
||||||
#profile {
|
#profile {
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { render, html } from '/uhtml.js';
|
||||||
|
|
||||||
let msg;
|
let msg;
|
||||||
|
|
||||||
function sendMessage(e) {
|
function sendMessage(event) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
if (!msg.value)
|
if (!msg.value)
|
||||||
return;
|
return;
|
||||||
window.emit('send_message', {
|
window.currentInstance.emit('send_message', {
|
||||||
message: msg.value,
|
message: msg.value,
|
||||||
thread: window.currentThread.id
|
thread: window.currentThread.id
|
||||||
});
|
});
|
||||||
|
@ -15,23 +15,9 @@ function sendMessage(e) {
|
||||||
|
|
||||||
let earliestMessage;
|
let earliestMessage;
|
||||||
|
|
||||||
window.socket.on('new_message', message => {
|
function loadMessages(callback) {
|
||||||
if (message.thread !== window.currentThread?.id)
|
let instance = window.currentInstance;
|
||||||
return;
|
|
||||||
const messages = document.getElementById('messages');
|
|
||||||
let scroll = messages.scrollHeight - messages.scrollTop <= messages.clientHeight;
|
|
||||||
messages.appendChild(html.node`
|
|
||||||
<div class='message'>
|
|
||||||
<strong>${message.name}: </strong>
|
|
||||||
${message.content}
|
|
||||||
</div>`);
|
|
||||||
if (scroll)
|
|
||||||
messages.scroll(0, messages.scrollHeight - messages.clientHeight);
|
|
||||||
if (!earliestMessage)
|
|
||||||
earliestMessage = message.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadMessages(firstRender, callback) {
|
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
render(document.getElementById('message'), html`
|
render(document.getElementById('message'), html`
|
||||||
<button id='loadmore' class='hidden' onclick=${loadMessages}>
|
<button id='loadmore' class='hidden' onclick=${loadMessages}>
|
||||||
|
@ -54,25 +40,36 @@ function loadMessages(firstRender, callback) {
|
||||||
else
|
else
|
||||||
document.getElementById('msginput').classList.add('hidden');
|
document.getElementById('msginput').classList.add('hidden');
|
||||||
}
|
}
|
||||||
window.emit('get_history', {
|
instance.emit('get_history', {
|
||||||
before: earliestMessage,
|
before: earliestMessage,
|
||||||
thread: window.currentThread.id
|
thread: window.currentThread.id
|
||||||
}, msg => {
|
}, async msg => {
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
console.log('get_history failed: ' + msg.message);
|
console.log('get_history failed: ' + msg.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback && callback(msg.messages);
|
callback && callback(msg.messages);
|
||||||
if (firstRender && messages.hasChildNodes())
|
|
||||||
return;
|
|
||||||
if (msg.messages.length > 0) {
|
if (msg.messages.length > 0) {
|
||||||
earliestMessage = msg.messages[msg.messages.length - 1].id;
|
earliestMessage = msg.messages[msg.messages.length - 1].id;
|
||||||
for (let message of msg.messages)
|
let users = {};
|
||||||
|
for (let message of msg.messages) {
|
||||||
|
if (!message.name) {
|
||||||
|
try {
|
||||||
|
message.name = (users[message.userid] || (
|
||||||
|
users[message.userid] = await window.getUser(message.userid)
|
||||||
|
)).displayname;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`error getting user ${message.userid}`, e);
|
||||||
|
message.name = message.userid;
|
||||||
|
}
|
||||||
|
}
|
||||||
messages.prepend(html.node`
|
messages.prepend(html.node`
|
||||||
<div class='message'>
|
<div class='message'>
|
||||||
<strong>${message.name}: </strong>
|
<strong>${message.name}: </strong>
|
||||||
${message.content}
|
${message.content}
|
||||||
</div>`);
|
</div>`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (msg.more)
|
if (msg.more)
|
||||||
document.getElementById('loadmore').classList.remove('hidden');
|
document.getElementById('loadmore').classList.remove('hidden');
|
||||||
|
@ -80,6 +77,25 @@ function loadMessages(firstRender, callback) {
|
||||||
document.getElementById('loadmore').classList.add('hidden');
|
document.getElementById('loadmore').classList.add('hidden');
|
||||||
messages.scroll(0, messages.scrollHeight - messages.clientHeight);
|
messages.scroll(0, messages.scrollHeight - messages.clientHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!instance.messaged) {
|
||||||
|
instance.socket.on('new_message', message => {
|
||||||
|
if (message.thread !== window.currentThread?.id || instance !== window.currentInstance)
|
||||||
|
return;
|
||||||
|
const messages = document.getElementById('messages');
|
||||||
|
let scroll = messages.scrollTop + 10 >= messages.scrollHeight - messages.clientHeight;
|
||||||
|
messages.appendChild(html.node`
|
||||||
|
<div class='message'>
|
||||||
|
<strong>${message.name}: </strong>
|
||||||
|
${message.content}
|
||||||
|
</div>`);
|
||||||
|
if (scroll)
|
||||||
|
messages.scroll(0, messages.scrollHeight - messages.clientHeight);
|
||||||
|
if (!earliestMessage)
|
||||||
|
earliestMessage = message.id;
|
||||||
|
});
|
||||||
|
instance.messaged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default loadMessages;
|
export default loadMessages;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
let space;
|
let space;
|
||||||
let spaceId;
|
|
||||||
|
|
||||||
let scale = 1;
|
let scale = 1; // todo: make zooming work
|
||||||
|
|
||||||
let editing;
|
let editing;
|
||||||
let dragging;
|
let dragging;
|
||||||
|
@ -26,8 +25,8 @@ function save(span) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
saving = true;
|
saving = true;
|
||||||
window.emit('save_span', {
|
window.currentInstance.emit('save_span', {
|
||||||
thread: spaceId,
|
thread: window.currentThread.id,
|
||||||
id: span.id ? span.id.slice(4) : '',
|
id: span.id ? span.id.slice(4) : '',
|
||||||
content: span.innerText,
|
content: span.innerText,
|
||||||
x: span.style.left.slice(0, -2),
|
x: span.style.left.slice(0, -2),
|
||||||
|
@ -71,8 +70,9 @@ function add(s) {
|
||||||
span.onblur = function(event) {
|
span.onblur = function(event) {
|
||||||
if (this.innerText)
|
if (this.innerText)
|
||||||
return;
|
return;
|
||||||
save(this);
|
|
||||||
this.remove();
|
this.remove();
|
||||||
|
if (this.id)
|
||||||
|
save(this);
|
||||||
};
|
};
|
||||||
span.onwheel = function(event) {
|
span.onwheel = function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -107,23 +107,10 @@ function add(s) {
|
||||||
return span;
|
return span;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.socket.on('span', msg => {
|
|
||||||
if (msg.thread !== spaceId)
|
|
||||||
return;
|
|
||||||
let span = document.getElementById('span' + msg.id);
|
|
||||||
if (span) {
|
|
||||||
span.innerText = msg.content;
|
|
||||||
span.x = msg.x;
|
|
||||||
span.y = msg.y;
|
|
||||||
span.scale = msg.scale;
|
|
||||||
span.style.transform = `translate(-50%, -50%) scale(${msg.scale})`;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
add(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function loadSpace(callback) {
|
export default function loadSpace(callback) {
|
||||||
if (!space) {
|
let instance = window.currentInstance;
|
||||||
|
|
||||||
|
if (!instance.spaceid) {
|
||||||
space = document.getElementById('space');
|
space = document.getElementById('space');
|
||||||
space.onmouseup = event => {
|
space.onmouseup = event => {
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
|
@ -143,18 +130,34 @@ export default function loadSpace(callback) {
|
||||||
});
|
});
|
||||||
editing.focus();
|
editing.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
instance.socket.on('span', msg => {
|
||||||
|
if (msg.thread !== instance.spaceid || window.currentInstance !== instance)
|
||||||
|
return;
|
||||||
|
let span = document.getElementById('span' + msg.id);
|
||||||
|
if (span) {
|
||||||
|
span.innerText = msg.content;
|
||||||
|
span.style.left = `${msg.x}px`;
|
||||||
|
span.style.top = `${msg.y}px`;
|
||||||
|
span.scale = msg.scale;
|
||||||
|
span.style.transform = `translate(-50%, -50%) scale(${msg.scale})`;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
add(msg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (spaceId === window.window.currentThread.id)
|
else if (instance.spaceid === window.currentThread.id)
|
||||||
return;
|
return;
|
||||||
spaceId = window.window.currentThread.id;
|
instance.spaceid = window.currentThread.id;
|
||||||
space.innerHTML = '';
|
space.innerHTML = '';
|
||||||
window.emit('get_space', { thread: window.window.currentThread.id }, msg => {
|
instance.emit('get_space', {
|
||||||
|
thread: window.currentThread.id
|
||||||
|
}, msg => {
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
console.log('get space failed: ' + msg.message);
|
console.log('get space failed: ' + msg.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
callback && callback(msg.spans);
|
callback && callback(msg.spans);
|
||||||
for (const span of msg.spans)
|
msg.spans.forEach(add);
|
||||||
add(span);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
233
client/stream.js
233
client/stream.js
|
@ -1,20 +1,17 @@
|
||||||
import { render, html } from '/uhtml.js';
|
import { render, html } from '/uhtml.js';
|
||||||
|
|
||||||
let streamid;
|
|
||||||
|
|
||||||
let handle;
|
|
||||||
let mediaStream;
|
let mediaStream;
|
||||||
let recorder;
|
|
||||||
|
|
||||||
async function stream() {
|
async function stream() {
|
||||||
if (handle) {
|
let thread = window.currentThread;
|
||||||
clearInterval(handle);
|
if (thread.handle) {
|
||||||
handle = null;
|
clearInterval(thread.handle);
|
||||||
if (recorder.state === 'recording')
|
delete thread.handle;
|
||||||
recorder.stop();
|
if (thread.recorder.state === 'recording')
|
||||||
window.emit('stream', {
|
thread.recorder.stop();
|
||||||
id: streamid,
|
thread.instance.emit('stream', {
|
||||||
thread: window.currentThread.id,
|
id: thread.streamid,
|
||||||
|
thread: thread.id,
|
||||||
stop: true
|
stop: true
|
||||||
});
|
});
|
||||||
document.getElementById('streaming').innerText = 'start streaming';
|
document.getElementById('streaming').innerText = 'start streaming';
|
||||||
|
@ -30,132 +27,148 @@ async function stream() {
|
||||||
sampleSize: 16
|
sampleSize: 16
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!mediaStream)
|
if (!mediaStream) {
|
||||||
|
console.log("couldn't get media stream");
|
||||||
return;
|
return;
|
||||||
window.emit('stream', {
|
}
|
||||||
thread: window.currentThread.id,
|
thread.instance.emit('stream', {
|
||||||
|
thread: thread.id,
|
||||||
name: document.getElementById('streamname').value
|
name: document.getElementById('streamname').value
|
||||||
}, msg => {
|
}, msg => {
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
console.log('stream failed: ', msg.message);
|
console.log('stream failed:', msg.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
streamid = msg.id;
|
thread.streamid = msg.id;
|
||||||
document.getElementById('streaming').innerText = 'stop streaming';
|
document.getElementById('streaming').innerText = 'stop streaming';
|
||||||
|
function record() {
|
||||||
|
let r = thread.recorder = new MediaRecorder(mediaStream);
|
||||||
|
let chunks = [];
|
||||||
|
r.ondataavailable = event => {
|
||||||
|
if (!event.data.size)
|
||||||
|
return;
|
||||||
|
chunks.push(event.data);
|
||||||
|
};
|
||||||
|
r.onstop = async () => {
|
||||||
|
if (!chunks.length || !thread.handle)
|
||||||
|
return;
|
||||||
|
//console.log(`${Date.now()} ${chunks.length}`);
|
||||||
|
thread.instance.emit('streamdata', {
|
||||||
|
id: thread.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(), 500);
|
||||||
|
};
|
||||||
|
r.start();
|
||||||
|
}
|
||||||
|
record();
|
||||||
|
thread.handle = setInterval(record, 500);
|
||||||
});
|
});
|
||||||
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(), 500);
|
|
||||||
};
|
|
||||||
r.start();
|
|
||||||
}
|
|
||||||
record();
|
|
||||||
handle = setInterval(record, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let audioctx;
|
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() {
|
function loadStreams() {
|
||||||
|
let instance = window.currentInstance;
|
||||||
|
|
||||||
let div = document.getElementById('stream');
|
let div = document.getElementById('stream');
|
||||||
div.innerHTML = '';
|
div.innerHTML = '';
|
||||||
if (window.currentThread.permissions.post) {
|
if (window.currentThread.permissions.post) {
|
||||||
// why doesn't html` work here? html.node` does
|
// why doesn't html` work here? html.node` does
|
||||||
render(div, html.node`
|
render(div, html.node`
|
||||||
<button id='streaming' onclick=${stream}>start streaming</button>
|
<button id='streaming' onclick=${stream}>
|
||||||
|
${window.currentThread.handle ? 'stop' : 'start'} streaming
|
||||||
|
</button>
|
||||||
<span>stream name:</span>
|
<span>stream name:</span>
|
||||||
<input id='streamname' oninput=${event => {
|
<input id='streamname' oninput=${function(event) {
|
||||||
if (handle)
|
if (window.currentThread.handle)
|
||||||
window.emit('stream', {
|
instance.emit('stream', {
|
||||||
id: streamid,
|
id: window.currentThread.streamid,
|
||||||
thread: window.currentThread.id,
|
thread: window.currentThread.id,
|
||||||
name: event.target.value
|
name: this.value
|
||||||
});
|
});
|
||||||
}}>`);
|
}}>`);
|
||||||
}
|
}
|
||||||
div.insertAdjacentHTML('beforeend', `
|
div.insertAdjacentHTML('beforeend', `
|
||||||
<p>streams:</p>
|
<p>streams:</p>
|
||||||
<div id='streams'></div>`);
|
<div id='streams'></div>`);
|
||||||
|
|
||||||
|
function addStream(stream) {
|
||||||
|
let p = html.node`
|
||||||
|
<p>
|
||||||
|
<button id='play' onclick=${e => {
|
||||||
|
if (stream.playing) {
|
||||||
|
audioctx.suspend();
|
||||||
|
delete instance.streaming[stream.id];
|
||||||
|
stream.playing = false;
|
||||||
|
e.target.innerText = '▶';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
audioctx = new AudioContext();
|
||||||
|
instance.streaming[stream.id] = stream;
|
||||||
|
stream.playing = true;
|
||||||
|
e.target.innerText = '⏹';
|
||||||
|
}
|
||||||
|
instance.emit('play_stream', {
|
||||||
|
id: stream.id,
|
||||||
|
thread: window.currentThread.id,
|
||||||
|
playing: stream.playing
|
||||||
|
}, msg => {
|
||||||
|
if (!msg.success)
|
||||||
|
console.log('play stream failed: ', msg.message);
|
||||||
|
});
|
||||||
|
}}>${instance.streaming[stream.id] ? '⏹' : '▶'}</button>
|
||||||
|
${stream.user}<span id='name'>${stream.name ? ` - ${stream.name}` : ''}</span>
|
||||||
|
</p>`;
|
||||||
|
p.id = 'stream' + stream.id;
|
||||||
|
document.getElementById('streams').append(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance.streaming) {
|
||||||
|
instance.streaming = {};
|
||||||
|
|
||||||
|
instance.socket.on('stream', async msg => {
|
||||||
|
let streams = document.querySelector(
|
||||||
|
`#instance${instance.id} > #threads > #threadlist > #thread${msg.thread}`)
|
||||||
|
.thread.streams;
|
||||||
|
let i = streams.findIndex(s => s.id === msg.id);
|
||||||
|
let p = document.getElementById('stream' + msg.id);
|
||||||
|
if (msg.stopped) {
|
||||||
|
if (i !== -1) {
|
||||||
|
streams.splice(i, 1);
|
||||||
|
if (msg.thread === window.currentThread?.id)
|
||||||
|
p.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (i === -1) {
|
||||||
|
streams.push(msg);
|
||||||
|
if (msg.thread === window.currentThread?.id)
|
||||||
|
addStream(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
streams[i].name = msg.name;
|
||||||
|
if (msg.thread === window.currentThread?.id)
|
||||||
|
p.children['name'].innerText = msg.name ? ' - ' + msg.name : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.socket.on('streamdata', async msg => {
|
||||||
|
if (!instance.streaming[msg.id])
|
||||||
|
return;
|
||||||
|
let source = audioctx.createBufferSource();
|
||||||
|
source.buffer = await audioctx.decodeAudioData(msg.audio);
|
||||||
|
source.connect(audioctx.destination);
|
||||||
|
source.start(/*audioStartTime*/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (let stream of window.currentThread.streams)
|
for (let stream of window.currentThread.streams)
|
||||||
addStream(stream);
|
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;
|
export default loadStreams;
|
||||||
|
|
|
@ -0,0 +1,353 @@
|
||||||
|
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 = `${
|
||||||
|
window.currentThread.permissions.everyone.view.value ?
|
||||||
|
'this thread is visible to everyone' :
|
||||||
|
'members can view this thread'}
|
||||||
|
${window.currentThread.permissions.everyone.post.value ?
|
||||||
|
'anyone can post' :
|
||||||
|
window.currentThread.permissions.members.post.value ?
|
||||||
|
'only members can post' : 'some members can post'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseThread() {
|
||||||
|
const edit = document.getElementById('edit');
|
||||||
|
let thread = this.thread;
|
||||||
|
if (window.currentThread) {
|
||||||
|
window.currentThread.div.classList.remove('active');
|
||||||
|
let editform = document.getElementById('editthread');
|
||||||
|
if (editform) {
|
||||||
|
editform.remove();
|
||||||
|
edit.textContent = 'edit';
|
||||||
|
}
|
||||||
|
if (window.currentThread.id === thread.id) {
|
||||||
|
document.getElementById('thread').classList.add('hidden');
|
||||||
|
window.currentThread = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
document.getElementById('thread').classList.remove('hidden');
|
||||||
|
if (thread.permissions.admin)
|
||||||
|
edit.classList.remove('hidden');
|
||||||
|
else
|
||||||
|
edit.classList.add('hidden');
|
||||||
|
document.getElementById('threadname').textContent = thread.name;
|
||||||
|
this.classList.add('active');
|
||||||
|
window.currentThread = thread;
|
||||||
|
window.currentInstance = thread.instance;
|
||||||
|
if (this.tab) {
|
||||||
|
switchTab(document.getElementById(this.tab));
|
||||||
|
if (this.tab === 'messagetab')
|
||||||
|
loadMessages();
|
||||||
|
else if (this.tab === 'spacetab')
|
||||||
|
loadSpace();
|
||||||
|
}
|
||||||
|
else // load first tab that has any content
|
||||||
|
loadMessages(messages => {
|
||||||
|
if (messages.length)
|
||||||
|
switchTab(document.getElementById(this.tab = 'messagetab'));
|
||||||
|
else
|
||||||
|
loadSpace(spans => {
|
||||||
|
if (spans.length)
|
||||||
|
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.currentInstance.emit('get_thread', { thread: thread.id }, async msg => {
|
||||||
|
if (!msg.success) {
|
||||||
|
console.log('get_thread failed:', msg.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(thread, msg.thread);
|
||||||
|
loadStreams();
|
||||||
|
setVisibility();
|
||||||
|
document.getElementById('memberlist').replaceChildren(
|
||||||
|
...await Promise.all(msg.thread.members.map(async member =>
|
||||||
|
html.node`<p class='member'>${
|
||||||
|
member.name || (await window.getUser(member.id)).name
|
||||||
|
}</p>`))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tab) {
|
||||||
|
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tabcontent').forEach(tab => tab.classList.add('hidden'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
document.getElementById(tab.id.slice(0, -3)).classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function newThread() {
|
||||||
|
let form = document.getElementById('createthread');
|
||||||
|
if (form) {
|
||||||
|
form.remove();
|
||||||
|
for (let create of document.querySelectorAll('#newthread')) {
|
||||||
|
create.textContent = 'create';
|
||||||
|
create.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instancediv = this.parentElement.parentElement;
|
||||||
|
|
||||||
|
let members = [{
|
||||||
|
id: String(window.id),
|
||||||
|
name: window.name,
|
||||||
|
permissions: { admin: 'true' }
|
||||||
|
}];
|
||||||
|
if (instancediv.instance !== window.instancelist[0]) {
|
||||||
|
members[0].id += '@' + location.host;
|
||||||
|
members[0].name += '@' + location.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMember() {
|
||||||
|
const name = document.getElementById('membername');
|
||||||
|
if (!name.value)
|
||||||
|
return;
|
||||||
|
let at = name.value.split('@');
|
||||||
|
let url = at[1] || instancediv.instance.url;
|
||||||
|
let user = await window.getUser('@' + url, at[0]);
|
||||||
|
if (instancediv.instance.url !== url) {
|
||||||
|
user.id += '@' + url;
|
||||||
|
user.name += '@' + url;
|
||||||
|
}
|
||||||
|
if (members.find(member => member.name === user.value))
|
||||||
|
return;
|
||||||
|
members.push(user);
|
||||||
|
document.getElementById('newmembers')
|
||||||
|
.append(html.node`<p class='member'>${user.name}</p>`);
|
||||||
|
name.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#home + .separator').insertAdjacentElement('afterend', html.node`
|
||||||
|
<form id='createthread' class='column' onsubmit=${event => {
|
||||||
|
event.preventDefault();
|
||||||
|
let name = document.getElementById('newthreadname').value;
|
||||||
|
if (!name) {
|
||||||
|
document.getElementById('newnameempty').classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const perms = document.querySelector(
|
||||||
|
'input[name="newpermissions"]:checked'
|
||||||
|
).value;
|
||||||
|
let permissions;
|
||||||
|
if (perms === 'public')
|
||||||
|
permissions = { view_limited: false, post_limited: false };
|
||||||
|
else if (perms === 'private_post')
|
||||||
|
permissions = { view_limited: false, post_limited: true };
|
||||||
|
else if (perms === 'private_view') {
|
||||||
|
permissions = { view_limited: true, post_limited: true };
|
||||||
|
// todo: generate key and encrypt
|
||||||
|
}
|
||||||
|
instancediv.instance.emit('create_thread',
|
||||||
|
{ name, permissions, members },
|
||||||
|
msg => {
|
||||||
|
if (!msg.success) {
|
||||||
|
console.log('create_thread error:', msg.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// since the form exists, this will perform cleanup
|
||||||
|
newThread();
|
||||||
|
chooseThread.call(instancediv.children['threads'].children['threadlist']
|
||||||
|
.children['thread' + msg.id]);
|
||||||
|
document.getElementById('loadmore').classList.add('hidden');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
<h3>create thread</h3>
|
||||||
|
<label for='newthreadname' class='heading'>thread name</label>
|
||||||
|
<p id='newnameempty' class='hidden'>name cannot be empty</p>
|
||||||
|
<input type='text' id='newthreadname' />
|
||||||
|
<p id='permissions'>thread permissions</p>
|
||||||
|
<input type='radio' name='newpermissions'
|
||||||
|
id='public' value='public' checked />
|
||||||
|
<label for='public'>anyone can view and post</label><br>
|
||||||
|
<input type='radio' name='newpermissions'
|
||||||
|
id='private_post' value='private_post' />
|
||||||
|
<label for='private_post'>anyone can view, only members can post</label><br>
|
||||||
|
<input type='radio' name='newpermissions'
|
||||||
|
id='private_view' value='private_view' />
|
||||||
|
<label for='private_view'>only members can view and post</label><br>
|
||||||
|
<label class='heading' for='membername'>members</label>
|
||||||
|
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
addMember();
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
<button id='addmember' type='button' onclick=${addMember}>add</button>
|
||||||
|
<div id='newmembers'>
|
||||||
|
<p class='member'>${members[0].name}</p>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<button id='submitthread' type='submit'>create</button>
|
||||||
|
</form>`);
|
||||||
|
for (let create of document.querySelectorAll('#newthread')) {
|
||||||
|
if (create === this)
|
||||||
|
create.textContent = 'cancel';
|
||||||
|
else
|
||||||
|
create.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editThread() {
|
||||||
|
let form = document.getElementById('editthread');
|
||||||
|
if (form) {
|
||||||
|
form.remove();
|
||||||
|
document.getElementById('edit').textContent = 'edit';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form = html.node`
|
||||||
|
<form id='editthread' class='column' onsubmit=${event => {
|
||||||
|
event.preventDefault();
|
||||||
|
let name = document.getElementById('editthreadname').value;
|
||||||
|
if (!name) {
|
||||||
|
document.getElementById('nameempty').classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const perms = document.querySelector(
|
||||||
|
'input[name="permissions"]:checked'
|
||||||
|
).value;
|
||||||
|
let permissions;
|
||||||
|
if (perms === 'public')
|
||||||
|
permissions = { view_limited: false, post_limited: false };
|
||||||
|
else if (perms === 'private_post')
|
||||||
|
permissions = { view_limited: false, post_limited: true };
|
||||||
|
else if (perms === 'private_view') {
|
||||||
|
permissions = { view_limited: true, post_limited: true };
|
||||||
|
// todo: generate key and encrypt
|
||||||
|
}
|
||||||
|
window.currentInstance.emit('edit_thread',
|
||||||
|
{ id: window.currentThread.id, name, permissions },
|
||||||
|
msg => {
|
||||||
|
if (!msg.success) {
|
||||||
|
console.log('edit_thread failed: ', msg.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editThread();
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<label for='editthreadname' class='heading'>thread name</label>
|
||||||
|
<input type='text' id='editthreadname' />
|
||||||
|
<p id='nameempty' class='hidden'>name cannot be empty</p>
|
||||||
|
<p id='permissions'>thread permissions</p>
|
||||||
|
<input type='radio' name='permissions'
|
||||||
|
id='public' value='public' />
|
||||||
|
<label for='public'>anyone can view and post</label><br>
|
||||||
|
<input type='radio' name='permissions'
|
||||||
|
id='private_post' value='private_post' />
|
||||||
|
<label for='private_post'>anyone can view, only members can post</label><br>
|
||||||
|
<input type='radio' name='permissions'
|
||||||
|
id='private_view' value='private_view' />
|
||||||
|
<label for='private_view'>only members can view and post</label><br>
|
||||||
|
<br>
|
||||||
|
<button id='savethread'>save</button>
|
||||||
|
</form>`;
|
||||||
|
form['editthreadname'].value = window.currentThread.name;
|
||||||
|
if (window.currentThread.permissions.everyone.post.value)
|
||||||
|
form['public'].checked = true;
|
||||||
|
else if (window.currentThread.permissions.everyone.view.value)
|
||||||
|
form['private_post'].checked = true;
|
||||||
|
else
|
||||||
|
form['private_view'].checked = true;
|
||||||
|
document.querySelector('#thread').append(form);
|
||||||
|
document.getElementById('edit').textContent = 'cancel';
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickedTab(event) {
|
||||||
|
switchTab(this);
|
||||||
|
document.getElementById(`thread${window.currentThread.id}`).tab = this.id;
|
||||||
|
if (this.id === 'messagetab')
|
||||||
|
loadMessages();
|
||||||
|
else if (this.id === 'spacetab')
|
||||||
|
loadSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadThreads(instancediv, select) {
|
||||||
|
function makeThread(thread) {
|
||||||
|
thread.instance = instancediv.instance;
|
||||||
|
let node = html.node`
|
||||||
|
<div class='thread' onclick=${chooseThread}>${
|
||||||
|
thread.name
|
||||||
|
}</div>`;
|
||||||
|
node.id = 'thread' + thread.id;
|
||||||
|
node.thread = thread;
|
||||||
|
thread.div = node;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!document.getElementById('thread'))
|
||||||
|
document.body.append(html.node`
|
||||||
|
<div id='thread' class='column hidden'>
|
||||||
|
<div id='content'>
|
||||||
|
<div id='titlebar'>
|
||||||
|
<span id='title'>thread: <strong id='threadname'>meow</strong></span>
|
||||||
|
<button id='edit' class='hidden' onclick=${editThread}>edit</button>
|
||||||
|
</div>
|
||||||
|
<div id='buttonbar'>
|
||||||
|
<div id='tabs'>
|
||||||
|
<button id='messagetab' class='tab active' onclick=${clickedTab}>messages
|
||||||
|
</button><button id='spacetab' class='tab' onclick=${clickedTab}>space
|
||||||
|
</button><button id='streamtab' class='tab' onclick=${clickedTab}>streams</button>
|
||||||
|
</div>
|
||||||
|
<button id='showmembers' onclick=${() =>
|
||||||
|
document.getElementById('members').classList.toggle('hidden')
|
||||||
|
}>members</button>
|
||||||
|
</div>
|
||||||
|
<div id='message' class='tabcontent'></div>
|
||||||
|
<div id='space' class='tabcontent hidden'></div>
|
||||||
|
<div id='stream' class='tabcontent hidden'></div>
|
||||||
|
</div>
|
||||||
|
<hr class='separator' color='#505050'>
|
||||||
|
<div id='members' class='hidden'>
|
||||||
|
<p id='visibility'></p>
|
||||||
|
<p><strong>members</strong></p>
|
||||||
|
<div id='memberlist'>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class='separator' color='#505050'>
|
||||||
|
</div>`);
|
||||||
|
|
||||||
|
if (!instancediv.children['threads']) {
|
||||||
|
instancediv.append(html.node`
|
||||||
|
<div id='threads'>
|
||||||
|
<p><strong>threads</strong></p>
|
||||||
|
<div id='threadlist'>loading...</div>
|
||||||
|
<button id='newthread' onclick=${newThread}>create</button>
|
||||||
|
</div>`);
|
||||||
|
const threadlist = instancediv.children['threads'].children['threadlist'];
|
||||||
|
|
||||||
|
instancediv.instance.emit('list_threads', {}, msg => {
|
||||||
|
threadlist.replaceChildren(...msg.threads.map(makeThread));
|
||||||
|
if (select && msg.threads.length)
|
||||||
|
chooseThread.call(threadlist.firstChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
instancediv.instance.socket.on('thread', thread => {
|
||||||
|
let el = threadlist.children['thread' + thread.id];
|
||||||
|
if (el) {
|
||||||
|
Object.assign(el.thread, thread);
|
||||||
|
el.textContent = thread.name;
|
||||||
|
if (window.currentThread?.id === thread.id) {
|
||||||
|
Object.assign(window.currentThread, thread);
|
||||||
|
document.getElementById('threadname').textContent = thread.name;
|
||||||
|
setVisibility();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
threadlist.prepend(makeThread(thread));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default loadThreads;
|
|
@ -16,38 +16,34 @@ create table key (
|
||||||
create table thread (
|
create table thread (
|
||||||
id integer primary key asc,
|
id integer primary key asc,
|
||||||
name text,
|
name text,
|
||||||
creator integer,
|
creator text,
|
||||||
created timestamp default current_timestamp,
|
created timestamp default current_timestamp
|
||||||
foreign key(creator) references user(id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
create table permission (
|
create table permission (
|
||||||
thread integer,
|
thread integer,
|
||||||
user integer,
|
user text,
|
||||||
type text,
|
type text,
|
||||||
mutable boolean,
|
mutable boolean,
|
||||||
permission text,
|
permission text,
|
||||||
value text,
|
value text,
|
||||||
foreign key(user) references user(id),
|
|
||||||
foreign key(thread) references thread(id)
|
foreign key(thread) references thread(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table member (
|
create table member (
|
||||||
thread integer,
|
thread integer,
|
||||||
user integer,
|
user text,
|
||||||
created timestamp default current_timestamp,
|
created timestamp default current_timestamp,
|
||||||
foreign key(user) references user(id),
|
|
||||||
foreign key(thread) references thread(id)
|
foreign key(thread) references thread(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table post (
|
create table post (
|
||||||
id integer primary key asc,
|
id integer primary key asc,
|
||||||
user integer,
|
user text,
|
||||||
thread integer,
|
thread integer,
|
||||||
content text,
|
content text,
|
||||||
encrypted bool,
|
encrypted bool,
|
||||||
created timestamp default current_timestamp,
|
created timestamp default current_timestamp,
|
||||||
foreign key(user) references user(id),
|
|
||||||
foreign key(thread) references thread(id)
|
foreign key(thread) references thread(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
63
index.js
63
index.js
|
@ -1,63 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const express = require('express');
|
|
||||||
const http = require('http');
|
|
||||||
const { Server } = require('socket.io');
|
|
||||||
const compression = require('compression');
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(compression());
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const io = new Server(server, {
|
|
||||||
cors: {
|
|
||||||
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) => {
|
|
||||||
if (!events[event]) {
|
|
||||||
callback('no such event ' + event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log('server running on port ' + PORT);
|
|
||||||
});
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "server",
|
"name": "vybe",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"openpgp": "^5.8.0",
|
"openpgp": "^5.8.0",
|
||||||
"socket.io": "^4.6.1",
|
"socket.io": "^4.6.1",
|
||||||
|
"socket.io-client": "^4.7.5",
|
||||||
"sqlite3": "^5.1.6"
|
"sqlite3": "^5.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const express = require('express');
|
||||||
|
const http = require('http');
|
||||||
|
const { Server } = require('socket.io');
|
||||||
|
const ioclient = require('socket.io-client');
|
||||||
|
const compression = require('compression');
|
||||||
|
|
||||||
|
const events = {};
|
||||||
|
for (let file of fs.readdirSync('./src/events')) {
|
||||||
|
file = require('./src/events/' + file);
|
||||||
|
for (const event in file)
|
||||||
|
events[event] = file[event];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rand32() {
|
||||||
|
let str = '';
|
||||||
|
const lookups = 'bcdefghjklmnpqrstvwxyz0123456789'.split('');
|
||||||
|
while (str.length < 16) {
|
||||||
|
const n = Math.random() * lookups.length;
|
||||||
|
str += lookups[Math.floor(n)];
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance;
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
fs.writeFileSync('instance.json', JSON.stringify(instance, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync('instance.json'))
|
||||||
|
instance = JSON.parse(fs.readFileSync('instance.json'));
|
||||||
|
else {
|
||||||
|
instance = {
|
||||||
|
id: rand32(),
|
||||||
|
port: process.env.PORT || 1312,
|
||||||
|
url: process.env.URL || null
|
||||||
|
};
|
||||||
|
saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instance.url)
|
||||||
|
console.log(
|
||||||
|
`!! no instance url !
|
||||||
|
it will be auto-set by the first user authentication !
|
||||||
|
it can be set in instance.json .`);
|
||||||
|
|
||||||
|
global.vybe = {
|
||||||
|
instance,
|
||||||
|
saveSettings,
|
||||||
|
instances: {},
|
||||||
|
users: {},
|
||||||
|
threads: {},
|
||||||
|
streams: {},
|
||||||
|
connectInstance: async url => {
|
||||||
|
let instance = vybe.instances[url];
|
||||||
|
function connecting(resolve, reject) {
|
||||||
|
instance.socket.on('connect', resolve);
|
||||||
|
instance.socket.on('connect_error', error => {
|
||||||
|
instance.socket.disconnect();
|
||||||
|
reject(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (instance) {
|
||||||
|
if (!instance.socket.connected) {
|
||||||
|
instance.socket.connect();
|
||||||
|
await new Promise(connecting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
instance = vybe.instances[url] = {
|
||||||
|
socket: ioclient('https://' + url)
|
||||||
|
};
|
||||||
|
await new Promise(connecting);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(compression());
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(express.static('client'));
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
for (let event in events) {
|
||||||
|
socket.on(event, (msg, callback) => {
|
||||||
|
if (!events[event]) {
|
||||||
|
callback('no such event ' + event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
events[event](msg, callback, socket);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(`${event} threw exception: `, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
socket.on('disconnect', reason => {
|
||||||
|
let user = vybe.users[socket.__userid];
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(instance.port, () => {
|
||||||
|
console.log('server running on port ' + instance.port);
|
||||||
|
});
|
|
@ -3,7 +3,7 @@ const db = require('./db');
|
||||||
const authwrap = (fn) => async (msg, respond, socket) => {
|
const authwrap = (fn) => async (msg, respond, socket) => {
|
||||||
if (!respond)
|
if (!respond)
|
||||||
respond = () => {};
|
respond = () => {};
|
||||||
if (!msg || !msg.__session || socket.auth !== msg.__session) {
|
if (!msg || !msg.__session || socket.__auth !== msg.__session) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'not authenticated'
|
message: 'not authenticated'
|
||||||
|
@ -11,7 +11,7 @@ const authwrap = (fn) => async (msg, respond, socket) => {
|
||||||
}
|
}
|
||||||
return await fn({
|
return await fn({
|
||||||
...msg,
|
...msg,
|
||||||
auth_user: vybe.users[socket.username]
|
auth_user: vybe.users[socket.__userid]
|
||||||
}, respond, socket);
|
}, respond, socket);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,9 @@ const check_permission = async (user_id, thread_id) => {
|
||||||
).rows.length > 0;
|
).rows.length > 0;
|
||||||
let perms = { is_member };
|
let perms = { is_member };
|
||||||
for (let p of permissions.rows) {
|
for (let p of permissions.rows) {
|
||||||
if (p.type === 'everyone' && p.value === 'true')
|
if ((p.type === 'everyone' && p.value === 'true')
|
||||||
perms[p.permission] = true;
|
|| (p.type === 'members' && is_member && p.value === 'true')
|
||||||
else if (p.type === 'members' && is_member && p.value === 'true')
|
|| (p.type === 'user' && p.user == user_id && p.value === 'true'))
|
||||||
perms[p.permission] = true;
|
|
||||||
else if (p.type === 'user' && p.user === user_id && p.value === 'true')
|
|
||||||
perms[p.permission] = true;
|
perms[p.permission] = true;
|
||||||
}
|
}
|
||||||
return perms;
|
return perms;
|
||||||
|
|
14
src/db.js
14
src/db.js
|
@ -1,9 +1,10 @@
|
||||||
const sqlite3 = require('sqlite3');
|
const sqlite3 = require('sqlite3');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const dbPath = 'vybe.db';
|
const path = 'vybe.db';
|
||||||
|
|
||||||
const db = new sqlite3.Database(dbPath);
|
const existed = fs.existsSync(path);
|
||||||
|
const db = new sqlite3.Database(path);
|
||||||
|
|
||||||
db.query = function (sql, params) {
|
db.query = function (sql, params) {
|
||||||
let self = this;
|
let self = this;
|
||||||
|
@ -18,11 +19,10 @@ db.query = function (sql, params) {
|
||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (fs.existsSync(dbPath))
|
if (!existed)
|
||||||
return;
|
for (let sql of fs.readFileSync('./db/1-init.sql').toString().split(';'))
|
||||||
for (let sql of fs.readFileSync('./db/1-init.sql').toString().split(';'))
|
if (sql.trim())
|
||||||
if (sql)
|
await db.query(sql);
|
||||||
await db.query(sql);
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|
|
@ -16,27 +16,25 @@ async function send_message(msg, respond) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// add message and send it to everyone
|
// add message and send it to everyone
|
||||||
const id = await db.query(
|
const id = (await db.query(
|
||||||
'insert into post (user, thread, content, encrypted) values (?, ?, ?, ?) returning id',
|
'insert into post (user, thread, content, encrypted) values (?, ?, ?, ?) returning id',
|
||||||
[msg.auth_user.id, msg.thread, msg.message, msg.encrypted]
|
[msg.auth_user.id, msg.thread, msg.message, msg.encrypted]
|
||||||
);
|
)).rows[0].id;
|
||||||
// get thread members
|
// get thread members
|
||||||
const members = (
|
const members = (
|
||||||
await db.query(
|
await db.query('select user from member where member.thread = ?', msg.thread)
|
||||||
'select name from user join member on member.user = user.id where member.thread = ?',
|
).rows.map(i => i.user);
|
||||||
[msg.thread]
|
|
||||||
)
|
|
||||||
).rows.map((i) => i.name);
|
|
||||||
// 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
|
||||||
[msg.thread]);
|
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`,
|
||||||
for (let username in vybe.users) {
|
msg.thread);
|
||||||
if (permissions.rows.length > 0 || members.includes(username)) {
|
for (let id in vybe.users) {
|
||||||
for (let s of vybe.users[username].sockets) {
|
if (permissions.rows.length > 0 || members.includes(id)) {
|
||||||
|
for (let s of vybe.users[id].sockets) {
|
||||||
s.emit('new_message', {
|
s.emit('new_message', {
|
||||||
id: id.rows[0].id,
|
id,
|
||||||
username: msg.auth_user.name,
|
userid: msg.auth_user.id,
|
||||||
name: msg.auth_user.displayname,
|
name: msg.auth_user.displayname,
|
||||||
content: msg.message,
|
content: msg.message,
|
||||||
thread: msg.thread
|
thread: msg.thread
|
||||||
|
@ -46,7 +44,7 @@ async function send_message(msg, respond) {
|
||||||
}
|
}
|
||||||
return respond({
|
return respond({
|
||||||
success: true,
|
success: true,
|
||||||
id: id.rows[0].id
|
id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,20 +67,20 @@ async function get_history(msg, respond) {
|
||||||
message: "you can't view this thread"
|
message: "you can't view this thread"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const messages = await db.query(
|
const messages = (await db.query(
|
||||||
`select coalesce(displayname, name) as name, post.id, content
|
`select coalesce(displayname, name) as name, user as userid, post.id, content
|
||||||
from post
|
from post
|
||||||
join user on post.user = user.id
|
left join user on post.user = user.id
|
||||||
where ${msg.before ? 'post.id < ? and' : ''}
|
where ${msg.before ? 'post.id < ? and' : ''}
|
||||||
thread = ?
|
thread = ?
|
||||||
order by post.created desc
|
order by post.created desc
|
||||||
limit 101`,
|
limit 101`,
|
||||||
msg.before ? [msg.before, msg.thread] : [msg.thread]
|
msg.before ? [msg.before, msg.thread] : [msg.thread]
|
||||||
);
|
)).rows;
|
||||||
return respond({
|
return respond({
|
||||||
success: true,
|
success: true,
|
||||||
messages: messages.rows.slice(0, 100),
|
messages: messages.slice(0, 100),
|
||||||
more: messages.rows.length > 100
|
more: messages.length > 100
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ async function get_space(msg, respond) {
|
||||||
}
|
}
|
||||||
const spans = await db.query(
|
const spans = await db.query(
|
||||||
'select id, content, x, y, scale from span where thread = ? and deleted = false',
|
'select id, content, x, y, scale from span where thread = ? and deleted = false',
|
||||||
[msg.thread]
|
msg.thread
|
||||||
);
|
);
|
||||||
return respond({
|
return respond({
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -43,10 +43,11 @@ async function save_span(msg, respond, socket) {
|
||||||
if (msg.id) {
|
if (msg.id) {
|
||||||
id = msg.id;
|
id = msg.id;
|
||||||
if (msg.content)
|
if (msg.content)
|
||||||
await db.query('update span set content=?, x=?, y=?, scale=?, deleted=false where id=?',
|
await db.query(
|
||||||
|
'update span set content = ?, x = ?, y = ?, scale = ?, deleted = false where id = ?',
|
||||||
[msg.content, msg.x, msg.y, msg.scale, msg.id]);
|
[msg.content, msg.x, msg.y, msg.scale, msg.id]);
|
||||||
else
|
else
|
||||||
await db.query('update span set deleted=true where id=?', [msg.id]);
|
await db.query('update span set deleted = true where id = ?', msg.id);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
id = (await db.query(
|
id = (await db.query(
|
||||||
|
@ -56,19 +57,17 @@ async function save_span(msg, respond, socket) {
|
||||||
}
|
}
|
||||||
// get thread members
|
// get thread members
|
||||||
const members = (
|
const members = (
|
||||||
await db.query(
|
await db.query('select user from member where member.thread = ?', msg.thread)
|
||||||
'select name from user join member on member.user = user.id where member.thread = ?',
|
).rows.map(i => i.user);
|
||||||
[msg.thread]
|
|
||||||
)
|
|
||||||
).rows.map(i => i.name);
|
|
||||||
// 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 = ?
|
||||||
[msg.thread]
|
and type = 'everyone' and value = 'true' and permission = 'view'`,
|
||||||
|
msg.thread
|
||||||
);
|
);
|
||||||
for (let username in vybe.users) {
|
for (let userid in vybe.users) {
|
||||||
if (permissions.rows.length > 0 || members.includes(username)) {
|
if (permissions.rows.length > 0 || members.includes(userid)) {
|
||||||
for (let s of vybe.users[username].sockets) {
|
for (let s of vybe.users[userid].sockets) {
|
||||||
if (s !== socket)
|
if (s !== socket)
|
||||||
s.emit('span', {
|
s.emit('span', {
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -17,25 +17,21 @@ async function stream(msg, respond, socket) {
|
||||||
});
|
});
|
||||||
async function send() {
|
async function send() {
|
||||||
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
|
||||||
[msg.thread]);
|
where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'`,
|
||||||
|
msg.thread);
|
||||||
if (permissions.rows.length) {
|
if (permissions.rows.length) {
|
||||||
for (let user in vybe.users) {
|
for (let id in vybe.users)
|
||||||
user = vybe.users[user];
|
for (let socket of vybe.users[id].sockets)
|
||||||
for (let socket of user.sockets)
|
|
||||||
socket.emit('stream', stream);
|
socket.emit('stream', stream);
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const members = (
|
for (let member of (
|
||||||
await db.query(
|
await db.query(
|
||||||
'select name from user join member on member.user = user.id where member.thread = ?',
|
'select user from member where member.thread = ?', msg.thread)
|
||||||
[msg.thread]
|
).rows) {
|
||||||
)
|
member = vybe.users[member.user];
|
||||||
).rows.map((i) => i.name);
|
if (member)
|
||||||
for (let member of members) {
|
for (let socket of member.sockets)
|
||||||
let user = vybe.users[member];
|
|
||||||
if (user)
|
|
||||||
for (let socket of user.sockets)
|
|
||||||
socket.emit('stream', stream);
|
socket.emit('stream', stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +43,11 @@ async function stream(msg, respond, socket) {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'stream not found'
|
message: 'stream not found'
|
||||||
});
|
});
|
||||||
|
if (msg.auth_user.id !== stream.userid)
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: "stream doesn't belong to user"
|
||||||
|
});
|
||||||
if (msg.stop) {
|
if (msg.stop) {
|
||||||
stream.stop();
|
stream.stop();
|
||||||
return respond({
|
return respond({
|
||||||
|
@ -66,6 +67,7 @@ async function stream(msg, respond, socket) {
|
||||||
thread.streams.push(stream);
|
thread.streams.push(stream);
|
||||||
vybe.streams[stream.id] = {
|
vybe.streams[stream.id] = {
|
||||||
stream,
|
stream,
|
||||||
|
userid: msg.auth_user.id,
|
||||||
listeners: {},
|
listeners: {},
|
||||||
socket: socket.id,
|
socket: socket.id,
|
||||||
stop: () => {
|
stop: () => {
|
||||||
|
@ -91,7 +93,7 @@ async function streamdata(msg, respond) {
|
||||||
message: 'stream not found'
|
message: 'stream not found'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (stream.stream.user !== msg.auth_user.name) {
|
if (stream.userid !== msg.auth_user.id) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: "stream doesn't belong to user"
|
message: "stream doesn't belong to user"
|
||||||
|
|
|
@ -14,11 +14,16 @@ async function create_thread(msg, respond) {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'thread name 200 chars max'
|
message: 'thread name 200 chars max'
|
||||||
});
|
});
|
||||||
if (!Array.isArray(msg.members) || msg.members.find(m => typeof m?.name !== 'string'))
|
if (!Array.isArray(msg.members) || msg.members.find(m => typeof m?.id !== 'string'))
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'invalid members'
|
message: 'invalid members'
|
||||||
});
|
});
|
||||||
|
if (msg.members.find(m => m.id == msg.auth_user.id)?.permissions?.admin !== 'true')
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: 'creator missing admin permission'
|
||||||
|
});
|
||||||
// add to db
|
// add to db
|
||||||
const thread_id = (await db.query(
|
const thread_id = (await db.query(
|
||||||
'insert into thread (name, creator) values (?, ?) returning id',
|
'insert into thread (name, creator) values (?, ?) returning id',
|
||||||
|
@ -59,25 +64,21 @@ async function create_thread(msg, respond) {
|
||||||
// add members
|
// add members
|
||||||
let members = {};
|
let members = {};
|
||||||
for (let member of msg.members) {
|
for (let member of msg.members) {
|
||||||
// get user id
|
let id = member.id.split('@');
|
||||||
const id = await db.query('select id from user where name = ?', [
|
id = id[1] === vybe.instance.url ? id[0] : member.id;
|
||||||
member.name,
|
if (members[id])
|
||||||
]);
|
|
||||||
if (id.rows.length === 0) {
|
|
||||||
console.log('user not found: ' + member.name);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
await db.query(
|
await db.query(
|
||||||
'insert into member (thread, user) values (?, ?)',
|
'insert into member (thread, user) values (?, ?)',
|
||||||
[thread_id, id.rows[0].id]
|
[thread_id, id]
|
||||||
);
|
);
|
||||||
members[member.name] = true;
|
members[id] = true;
|
||||||
if (typeof member.permissions === 'object')
|
if (typeof member.permissions === 'object')
|
||||||
for (let permission in member.permissions)
|
for (let permission in member.permissions)
|
||||||
await db.query(`
|
await db.query(`
|
||||||
insert into permission (thread, type, user, mutable, permission, value)
|
insert into permission (thread, type, user, mutable, permission, value)
|
||||||
values (?, ?, ?, ?, ?, ?)`,
|
values (?, ?, ?, ?, ?, ?)`,
|
||||||
[thread_id, 'user', id.rows[0].id, true, permission, member.permissions[permission]]);
|
[thread_id, 'user', id, true, permission, member.permissions[permission]]);
|
||||||
}
|
}
|
||||||
let thread = {
|
let thread = {
|
||||||
id: thread_id,
|
id: thread_id,
|
||||||
|
@ -85,30 +86,31 @@ async function create_thread(msg, respond) {
|
||||||
streams: []
|
streams: []
|
||||||
};
|
};
|
||||||
if (!msg.permissions?.view_limited) {
|
if (!msg.permissions?.view_limited) {
|
||||||
for (let username in vybe.users) {
|
for (let id in vybe.users) {
|
||||||
for (let socket of vybe.users[username].sockets) {
|
for (let socket of vybe.users[id].sockets) {
|
||||||
socket.emit('thread', {
|
socket.emit('thread', {
|
||||||
...thread,
|
...thread,
|
||||||
permissions: {
|
permissions: {
|
||||||
view: true,
|
view: true,
|
||||||
post: !msg.permissions?.post_limited || members[username],
|
post: !msg.permissions?.post_limited || members[id],
|
||||||
admin: username === msg.auth_user.name
|
admin: id === msg.auth_user.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (let member of msg.members) {
|
for (let member in members) {
|
||||||
if (!vybe.users[member.name])
|
member = vybe.users[member];
|
||||||
|
if (!member)
|
||||||
continue;
|
continue;
|
||||||
for (let socket of vybe.users[member.name].sockets) {
|
for (let socket of member.sockets) {
|
||||||
socket.emit('thread', {
|
socket.emit('thread', {
|
||||||
...thread,
|
...thread,
|
||||||
permissions: {
|
permissions: {
|
||||||
view: true,
|
view: true,
|
||||||
post: true,
|
post: true,
|
||||||
admin: member.name === msg.auth_user.name
|
admin: member.id === msg.auth_user.id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -127,13 +129,12 @@ async function list_threads(msg, respond) {
|
||||||
`select name, id from thread
|
`select name, id from thread
|
||||||
join permission on thread.id = permission.thread
|
join permission on thread.id = permission.thread
|
||||||
left join member on thread.id = member.thread
|
left join member on thread.id = member.thread
|
||||||
where permission.permission = 'view'
|
where permission.permission = 'view' and permission.value = 'true'
|
||||||
and permission.value = 'true'
|
|
||||||
and ((permission.type = 'everyone') or
|
and ((permission.type = 'everyone') or
|
||||||
permission.type = 'members' and member.user = ?)
|
permission.type = 'members' and member.user = ?)
|
||||||
group by thread.id
|
group by thread.id
|
||||||
order by thread.created desc`,
|
order by thread.created desc`,
|
||||||
[msg.auth_user.id]
|
msg.auth_user.id
|
||||||
);
|
);
|
||||||
threads = await Promise.all(threads.rows.map(async thread => {
|
threads = await Promise.all(threads.rows.map(async thread => {
|
||||||
thread.streams = vybe.threads[thread.id]?.streams || [];
|
thread.streams = vybe.threads[thread.id]?.streams || [];
|
||||||
|
@ -161,19 +162,25 @@ async function get_thread(msg, respond) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let thread = await db.query(
|
let thread = await db.query(
|
||||||
`select thread.name, user.name as user, user.id from thread
|
`select thread.name, member.user, user.name as username,
|
||||||
|
coalesce(user.displayname, user.name) as displayname
|
||||||
|
from thread
|
||||||
left join member on thread.id = member.thread
|
left join member on thread.id = member.thread
|
||||||
left join user on user.id = member.user
|
left join user on user.id = member.user
|
||||||
where thread.id = ?`,
|
where thread.id = ?`,
|
||||||
[msg.thread]
|
msg.thread
|
||||||
);
|
);
|
||||||
const permissions = await db.query(
|
const permissions = await db.query(
|
||||||
`select type, user, permission, value, mutable
|
`select type, user, permission, value, mutable
|
||||||
from permission where thread = ?`,
|
from permission where thread = ?`,
|
||||||
[msg.thread]
|
msg.thread
|
||||||
);
|
);
|
||||||
let members = Object.fromEntries(thread.rows.map(member =>
|
let members = Object.fromEntries(thread.rows.map(member =>
|
||||||
[member.id, { name: member.user }]
|
[member.user, {
|
||||||
|
id: member.user,
|
||||||
|
name: member.username,
|
||||||
|
displayname: member.displayname
|
||||||
|
}]
|
||||||
));
|
));
|
||||||
for (let permission of permissions.rows) {
|
for (let permission of permissions.rows) {
|
||||||
const member = members[permission.user];
|
const member = members[permission.user];
|
||||||
|
@ -207,11 +214,8 @@ async function get_thread(msg, respond) {
|
||||||
id: msg.thread,
|
id: msg.thread,
|
||||||
name: thread.rows[0].name,
|
name: thread.rows[0].name,
|
||||||
permissions: perms,
|
permissions: perms,
|
||||||
members: Object.entries(members).map(member => ({
|
members: Object.values(members),
|
||||||
id: member[0],
|
streams: vybe.threads[msg.thread]?.streams || []
|
||||||
...member[1]
|
|
||||||
})),
|
|
||||||
streams: vybe.threads[thread.id]?.streams || []
|
|
||||||
};
|
};
|
||||||
return respond({
|
return respond({
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -251,7 +255,7 @@ async function edit_thread(msg, respond) {
|
||||||
`select type, permission, value, mutable
|
`select type, permission, value, mutable
|
||||||
from permission
|
from permission
|
||||||
where type != 'user' and thread = ?`,
|
where type != 'user' and thread = ?`,
|
||||||
[msg.id]
|
msg.id
|
||||||
)).rows) {
|
)).rows) {
|
||||||
(permissions[p.type] || (permissions[p.type] = {}))
|
(permissions[p.type] || (permissions[p.type] = {}))
|
||||||
[p.permission] = {
|
[p.permission] = {
|
||||||
|
@ -324,41 +328,41 @@ async function edit_thread(msg, respond) {
|
||||||
permissions.members.view.value = permissions.members.view.value === 'true';
|
permissions.members.view.value = permissions.members.view.value === 'true';
|
||||||
permissions.members.post.value = permissions.members.post.value === 'true';
|
permissions.members.post.value = permissions.members.post.value === 'true';
|
||||||
let members = Object.fromEntries((await db.query(
|
let members = Object.fromEntries((await db.query(
|
||||||
`select user.name, user.id from thread
|
`select member.user from thread
|
||||||
join member on thread.id = member.thread
|
join member on thread.id = member.thread
|
||||||
join user on user.id = member.user
|
|
||||||
where thread.id = ?`,
|
where thread.id = ?`,
|
||||||
[msg.id]
|
msg.id
|
||||||
)).rows.map(row => [row.name, row.id]));
|
)).rows.map(row => [row.user, true]));
|
||||||
let thread = {
|
let thread = {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
name: msg.name,
|
name: msg.name,
|
||||||
streams: vybe.threads[msg.id]?.streams || []
|
streams: vybe.threads[msg.id]?.streams || []
|
||||||
};
|
};
|
||||||
if (!msg.permissions?.view_limited) {
|
if (!msg.permissions?.view_limited) {
|
||||||
for (let username in vybe.users)
|
for (let id in vybe.users)
|
||||||
for (let socket of vybe.users[username].sockets)
|
for (let socket of vybe.users[id].sockets)
|
||||||
socket.emit('thread', {
|
socket.emit('thread', {
|
||||||
...thread,
|
...thread,
|
||||||
permissions: {
|
permissions: {
|
||||||
view: true,
|
view: true,
|
||||||
post: !msg.permissions?.post_limited || username in members,
|
post: !msg.permissions?.post_limited || id in members,
|
||||||
admin: username === msg.auth_user.name && perms.admin,
|
admin: id === msg.auth_user.id && perms.admin,
|
||||||
...permissions
|
...permissions
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (let membername in members) {
|
for (let member in members) {
|
||||||
if (!vybe.users[membername])
|
member = vybe.users[member];
|
||||||
|
if (!member)
|
||||||
continue;
|
continue;
|
||||||
for (let socket of vybe.users[membername].sockets)
|
for (let socket of member.sockets)
|
||||||
socket.emit('thread', {
|
socket.emit('thread', {
|
||||||
...thread,
|
...thread,
|
||||||
permissions: {
|
permissions: {
|
||||||
view: true,
|
view: true,
|
||||||
post: true,
|
post: true,
|
||||||
admin: membername === msg.auth_user.name && perms.admin,
|
admin: member.id === msg.auth_user.id && perms.admin,
|
||||||
...permissions
|
...permissions
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,16 +4,16 @@ const openpgp = require('openpgp');
|
||||||
|
|
||||||
async function create_user(msg, respond) {
|
async function create_user(msg, respond) {
|
||||||
// validate inputs
|
// validate inputs
|
||||||
if (!msg.name) {
|
if (typeof msg.name !== 'string') {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'username required'
|
message: 'invalid username'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!msg.pubkey) {
|
if (typeof msg.pubkey !== 'string') {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'public key required'
|
message: 'invalid public key'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// ensure username is not taken
|
// ensure username is not taken
|
||||||
|
@ -53,36 +53,41 @@ async function create_user(msg, respond) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUser(id, name) {
|
||||||
|
return (id ? await db.query(
|
||||||
|
`select name, id, displayname from user where id = ?`, id)
|
||||||
|
: await db.query(
|
||||||
|
`select name, id, displayname from user where name = ?`, name)
|
||||||
|
).rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
async function authenticate(msg, respond, socket) {
|
async function authenticate(msg, respond, socket) {
|
||||||
if (!msg.name || !msg.message) {
|
if (typeof msg.name !== 'string'
|
||||||
|
|| typeof msg.id !== 'string'
|
||||||
|
|| typeof msg.message !== 'string') {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'invalid message'
|
message: 'invalid message'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let user = (await db.query(
|
let user;
|
||||||
`select id, displayname from user where name = ?`, [msg.name])).rows[0];
|
let id = msg.id.split('@');
|
||||||
if (!user) {
|
if (id.length !== 2 || !id[1])
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'user not found'
|
message: 'invalid user id'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
let result = await db.query(
|
|
||||||
`select user.id from user
|
|
||||||
join key on key.user = user.id
|
|
||||||
where name = ? and pubkey = ? and active = true`,
|
|
||||||
[msg.name, msg.pubkey]
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
const key = await openpgp.readKey({ armoredKey: msg.pubkey });
|
// verify key and get session auth string
|
||||||
|
let pubkey = msg.pubkey;
|
||||||
|
const key = await openpgp.readKey({ armoredKey: pubkey });
|
||||||
const verification = await openpgp.verify({
|
const verification = await openpgp.verify({
|
||||||
message: await openpgp.readCleartextMessage({
|
message: await openpgp.readCleartextMessage({
|
||||||
cleartextMessage: msg.message
|
cleartextMessage: msg.message
|
||||||
}),
|
}),
|
||||||
verificationKeys: key,
|
verificationKeys: key,
|
||||||
expectSigned: true,
|
expectSigned: true,
|
||||||
date: new Date(Date.now() + 60000 * 4) // slightly in the future to compensate for some system times
|
date: new Date(Date.now() + 60000 * 4) // slightly in the future to compensate for some system clocks
|
||||||
});
|
});
|
||||||
const data = verification.data.split(' ');
|
const data = verification.data.split(' ');
|
||||||
if (data[0] !== 'vybe_auth') {
|
if (data[0] !== 'vybe_auth') {
|
||||||
|
@ -91,41 +96,93 @@ async function authenticate(msg, respond, socket) {
|
||||||
message: 'bad auth message'
|
message: 'bad auth message'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (vybe.instance.url === id[1] || (!vybe.instance.url && !id[0])) {
|
||||||
|
// this should be user's home instance
|
||||||
|
user = await getUser(id[0], msg.name);
|
||||||
|
if (!user) {
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: 'user not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let result = await db.query(
|
||||||
|
`select user.id from user
|
||||||
|
join key on key.user = user.id
|
||||||
|
where name = ? and pubkey = ? and active = true`,
|
||||||
|
[msg.name, pubkey]
|
||||||
|
);
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
// request auth from logged in sessions
|
||||||
|
let id = key.getFingerprint().slice(0, 8);
|
||||||
|
let time = Date.now();
|
||||||
|
if (!await new Promise(resolve => {
|
||||||
|
user.authrequests[id] = { time, callback: resolve };
|
||||||
|
for (let s of user.sockets)
|
||||||
|
s.emit('authrequest', { id, time });
|
||||||
|
setTimeout(() => {
|
||||||
|
delete user.authrequests[id];
|
||||||
|
resolve(false);
|
||||||
|
}, 60000 * 5);
|
||||||
|
}))
|
||||||
|
return;
|
||||||
|
delete user.authrequests[id];
|
||||||
|
if (Date.now() - time > 60000 * 5)
|
||||||
|
return;
|
||||||
|
await db.query(
|
||||||
|
'insert into key (user, pubkey, active) values (?, ?, true)',
|
||||||
|
[user.id, pubkey]);
|
||||||
|
}
|
||||||
|
// default instance url to first authenticated user's location.host
|
||||||
|
if (!vybe.instance.url) {
|
||||||
|
vybe.instance.url = id[1];
|
||||||
|
vybe.saveSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// connect to user's home instance and ask for their key
|
||||||
|
try {
|
||||||
|
let instance = await vybe.connectInstance(id[1]);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
instance.socket.emit('get_keys', { ids: [id[0]] }, msg => {
|
||||||
|
if (!msg.success) {
|
||||||
|
console.log('get_keys error: ' + msg.message);
|
||||||
|
return reject('get_keys error: ' + msg.message);
|
||||||
|
}
|
||||||
|
if (!Array.isArray(msg.keys?.[id[0]]))
|
||||||
|
return reject('got invalid keys');
|
||||||
|
if (msg.keys[id[0]].indexOf(pubkey) === -1)
|
||||||
|
return reject('pubkey not authenticated');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
user = { id: msg.id, name: msg.name };
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(`error authenticating with ${id[1]}: ${error}`);
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: `error authenticating with ${id[1]}: ${error}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// this socket is now authenticated
|
||||||
if (!user.displayname)
|
if (!user.displayname)
|
||||||
user.displayname = msg.name;
|
user.displayname = user.name;
|
||||||
user = vybe.users[msg.name] || (vybe.users[msg.name] = {
|
user = vybe.users[user.id] || (vybe.users[user.id] = {
|
||||||
...user,
|
...user,
|
||||||
name: msg.name,
|
|
||||||
sockets: [],
|
sockets: [],
|
||||||
authrequests: {}
|
authrequests: {}
|
||||||
});
|
});
|
||||||
if (result.rows.length === 0) {
|
socket.__auth = data[1];
|
||||||
// request auth from logged in sessions
|
socket.__userid = user.id;
|
||||||
let id = key.getFingerprint().slice(0, 8);
|
|
||||||
let time = Date.now();
|
|
||||||
if (!await new Promise(resolve => {
|
|
||||||
user.authrequests[id] = { time, callback: resolve };
|
|
||||||
for (let s of user.sockets)
|
|
||||||
s.emit('authrequest', { id, time });
|
|
||||||
setTimeout(() => {
|
|
||||||
delete user.authrequests[id];
|
|
||||||
resolve(false);
|
|
||||||
}, 60000 * 5);
|
|
||||||
}))
|
|
||||||
return;
|
|
||||||
delete user.authrequests[id];
|
|
||||||
if (Date.now() - time > 60000 * 5)
|
|
||||||
return;
|
|
||||||
await db.query(
|
|
||||||
'insert into key (user, pubkey, active) values (?, ?, true)',
|
|
||||||
[user.id, msg.pubkey]);
|
|
||||||
}
|
|
||||||
// this socket is now authenticated
|
|
||||||
socket.auth = data[1];
|
|
||||||
socket.username = msg.name;
|
|
||||||
user.sockets.push(socket);
|
user.sockets.push(socket);
|
||||||
respond({
|
respond({
|
||||||
success: true,
|
success: true,
|
||||||
|
instance: {
|
||||||
|
id: vybe.instance.id
|
||||||
|
},
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
displayname: user.displayname,
|
displayname: user.displayname,
|
||||||
authrequests: Object.entries(user.authrequests).map(authrequest => ({
|
authrequests: Object.entries(user.authrequests).map(authrequest => ({
|
||||||
id: authrequest[0],
|
id: authrequest[0],
|
||||||
|
@ -143,7 +200,7 @@ async function authenticate(msg, respond, socket) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function authorize_key(msg, respond) {
|
async function authorize_key(msg, respond) {
|
||||||
let authrequest = vybe.users[msg.auth_user.name].authrequests[msg.id];
|
let authrequest = vybe.users[msg.auth_user.id].authrequests[msg.id];
|
||||||
if (!authrequest) {
|
if (!authrequest) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -158,43 +215,58 @@ async function authorize_key(msg, respond) {
|
||||||
|
|
||||||
async function update_user(msg, respond) {
|
async function update_user(msg, respond) {
|
||||||
if ((await db.query(
|
if ((await db.query(
|
||||||
`update user set displayname = ?
|
`select id from user where id = ?`, msg.auth_user.id)).rows.length === 0)
|
||||||
where name = ?
|
|
||||||
returning id`,
|
|
||||||
[msg.displayname, msg.auth_user.name])).rows.length === 0)
|
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'user not found'
|
message: 'user not found'
|
||||||
});
|
});
|
||||||
vybe.users[msg.auth_user.name].displayname = msg.displayname;
|
await db.query(
|
||||||
|
`update user set displayname = ? where id = ?`,
|
||||||
|
[msg.displayname, msg.auth_user.id]);
|
||||||
|
vybe.users[msg.auth_user.id].displayname = msg.displayname;
|
||||||
respond({
|
respond({
|
||||||
success: true
|
success: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function get_user(msg, respond) {
|
||||||
|
if (typeof msg.name !== 'string' && typeof msg.id !== 'string')
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: 'name or id required'
|
||||||
|
});
|
||||||
|
let user = await getUser(msg.id, msg.name);
|
||||||
|
if (!user)
|
||||||
|
return respond({
|
||||||
|
success: false,
|
||||||
|
message: 'user not found'
|
||||||
|
});
|
||||||
|
if (!user.displayname)
|
||||||
|
user.displayname = user.name;
|
||||||
|
return respond({
|
||||||
|
success: true,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function get_keys(msg, respond) {
|
async function get_keys(msg, respond) {
|
||||||
// validate inputs
|
// validate inputs
|
||||||
if (!msg.names) {
|
if (!Array.isArray(msg.ids)) {
|
||||||
return respond({
|
return respond({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'user names required'
|
message: 'invalid ids'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (typeof msg.names !== 'object') {
|
let keys = Object.fromEntries(msg.ids.map(id => [id, []]));
|
||||||
return respond({
|
(await db.query(
|
||||||
success: false,
|
`select pubkey, user from key where user in
|
||||||
message: "can't iterate user names"
|
(${msg.ids.map(i => '?').join(',')})`,
|
||||||
});
|
msg.ids
|
||||||
}
|
)).rows.forEach(key => keys[key.user].push(key.pubkey));
|
||||||
const keys = await db.query( // todo: encryption !
|
|
||||||
`select name from user where name in
|
|
||||||
(${msg.names.map((i) => '?').join(',')})`,
|
|
||||||
msg.names
|
|
||||||
);
|
|
||||||
// respond
|
// respond
|
||||||
respond({
|
respond({
|
||||||
success: true,
|
success: true,
|
||||||
keys: keys.rows
|
keys
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,5 +275,6 @@ module.exports = {
|
||||||
authenticate,
|
authenticate,
|
||||||
authorize_key: authwrap(authorize_key),
|
authorize_key: authwrap(authorize_key),
|
||||||
update_user: authwrap(update_user),
|
update_user: authwrap(update_user),
|
||||||
get_keys: authwrap(get_keys)
|
get_user,
|
||||||
|
get_keys
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# EditorConfig helps developers define and maintain consistent
|
|
||||||
# coding styles between different editors and IDEs
|
|
||||||
# editorconfig.org
|
|
||||||
|
|
||||||
root = true
|
|
||||||
|
|
||||||
|
|
||||||
[*]
|
|
||||||
|
|
||||||
# Change these settings to your own preference
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
# We recommend you to keep these unchanged
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.md]
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.json]
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.{html,js,md}]
|
|
||||||
block_comment_start = /**
|
|
||||||
block_comment = *
|
|
||||||
block_comment_end = */
|
|
|
@ -1,24 +0,0 @@
|
||||||
## editors
|
|
||||||
/.idea
|
|
||||||
/.vscode
|
|
||||||
|
|
||||||
## system files
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
## npm
|
|
||||||
/node_modules/
|
|
||||||
/npm-debug.log
|
|
||||||
|
|
||||||
## testing
|
|
||||||
/coverage/
|
|
||||||
|
|
||||||
## temp folders
|
|
||||||
/.tmp/
|
|
||||||
|
|
||||||
# build
|
|
||||||
/_site/
|
|
||||||
/dist/
|
|
||||||
/out-tsc/
|
|
||||||
|
|
||||||
storybook-static
|
|
||||||
custom-elements.json
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 vyber-client
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,31 +0,0 @@
|
||||||
<p align="center">
|
|
||||||
<img width="200" src="https://open-wc.org/hero.png"></img>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Open-wc Starter App
|
|
||||||
|
|
||||||
[![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](https://github.com/open-wc)
|
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
To get started:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm init @open-wc
|
|
||||||
# requires node 10 & npm 6 or higher
|
|
||||||
```
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
- `start` runs your app for development, reloading on file changes
|
|
||||||
- `start:build` runs your app after it has been built using the build command
|
|
||||||
- `build` builds your app and outputs it in your `dist` directory
|
|
||||||
- `test` runs your test suite with Web Test Runner
|
|
||||||
- `lint` runs the linter for your project
|
|
||||||
- `format` fixes linting and formatting errors
|
|
||||||
|
|
||||||
## Tooling configs
|
|
||||||
|
|
||||||
For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
|
|
||||||
|
|
||||||
If you customize the configuration a lot, you can consider moving them to individual files.
|
|
|
@ -1,29 +0,0 @@
|
||||||
<svg
|
|
||||||
width="244px"
|
|
||||||
height="244px"
|
|
||||||
viewBox="0 0 244 244"
|
|
||||||
version="1.1"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
|
||||||
<stop stop-color="#9B00FF" offset="0%"></stop>
|
|
||||||
<stop stop-color="#0077FF" offset="100%"></stop>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
id="Page-1"
|
|
||||||
stroke="none"
|
|
||||||
stroke-width="1"
|
|
||||||
fill="none"
|
|
||||||
fill-rule="evenodd"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M205.639259,176.936244 C207.430887,174.217233 209.093339,171.405629 210.617884,168.510161 M215.112174,158.724316 C216.385153,155.50304 217.495621,152.199852 218.433474,148.824851 M220.655293,138.874185 C221.231935,135.482212 221.637704,132.03207 221.863435,128.532919 M222,118.131039 C221.860539,114.466419 221.523806,110.85231 221.000113,107.299021 M218.885321,96.8583653 C218.001583,93.4468963 216.942225,90.1061026 215.717466,86.8461994 M211.549484,77.3039459 C209.957339,74.1238901 208.200597,71.0404957 206.290425,68.0649233 M200.180513,59.5598295 C181.848457,36.6639805 153.655709,22 122.036748,22 C66.7879774,22 22,66.771525 22,122 C22,177.228475 66.7879774,222 122.036748,222 C152.914668,222 180.52509,208.015313 198.875424,186.036326"
|
|
||||||
id="Shape"
|
|
||||||
stroke="url(#linearGradient-1)"
|
|
||||||
stroke-width="42.0804674"
|
|
||||||
></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,28 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
||||||
<meta name="Description" content="Put your description here.">
|
|
||||||
<base href="/">
|
|
||||||
|
|
||||||
<style>
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
background-color: #ededed;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<title>vyber-client</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<vyber-client></vyber-client>
|
|
||||||
|
|
||||||
<script type="module" src="./src/vyber-client.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"name": "vyber-client",
|
|
||||||
"description": "Webcomponent vyber-client following open-wc recommendations",
|
|
||||||
"license": "MIT",
|
|
||||||
"author": "vyber-client",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "rimraf dist && rollup -c rollup.config.js && npm run analyze -- --exclude dist",
|
|
||||||
"start:build": "web-dev-server --root-dir dist --app-index index.html --open",
|
|
||||||
"analyze": "cem analyze --litelement",
|
|
||||||
"start": "web-dev-server"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lit": "^2.0.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/preset-env": "^7.16.4",
|
|
||||||
"@custom-elements-manifest/analyzer": "^0.4.17",
|
|
||||||
"@open-wc/building-rollup": "^2.0.2",
|
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^13.0.6",
|
|
||||||
"@web/dev-server": "^0.1.34",
|
|
||||||
"@web/rollup-plugin-html": "^1.11.0",
|
|
||||||
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
|
|
||||||
"babel-plugin-template-html-minifier": "^4.1.0",
|
|
||||||
"deepmerge": "^4.2.2",
|
|
||||||
"esbuild": "^0.17.19",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"rollup": "^2.60.0",
|
|
||||||
"rollup-plugin-esbuild": "^5.0.0",
|
|
||||||
"rollup-plugin-workbox": "^6.2.0"
|
|
||||||
},
|
|
||||||
"customElements": "custom-elements.json"
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
|
||||||
import babel from '@rollup/plugin-babel';
|
|
||||||
import html from '@web/rollup-plugin-html';
|
|
||||||
import { importMetaAssets } from '@web/rollup-plugin-import-meta-assets';
|
|
||||||
import esbuild from 'rollup-plugin-esbuild';
|
|
||||||
import { generateSW } from 'rollup-plugin-workbox';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
input: 'index.html',
|
|
||||||
output: {
|
|
||||||
entryFileNames: '[hash].js',
|
|
||||||
chunkFileNames: '[hash].js',
|
|
||||||
assetFileNames: '[hash][extname]',
|
|
||||||
format: 'es',
|
|
||||||
dir: 'dist',
|
|
||||||
},
|
|
||||||
preserveEntrySignatures: false,
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
/** Enable using HTML as rollup entrypoint */
|
|
||||||
html({
|
|
||||||
minify: true,
|
|
||||||
injectServiceWorker: true,
|
|
||||||
serviceWorkerPath: 'dist/sw.js',
|
|
||||||
}),
|
|
||||||
/** Resolve bare module imports */
|
|
||||||
nodeResolve(),
|
|
||||||
/** Minify JS, compile JS to a lower language target */
|
|
||||||
esbuild({
|
|
||||||
minify: true,
|
|
||||||
target: ['chrome64', 'firefox67', 'safari11.1'],
|
|
||||||
}),
|
|
||||||
/** Bundle assets references via import.meta.url */
|
|
||||||
importMetaAssets(),
|
|
||||||
/** Minify html and css tagged template literals */
|
|
||||||
babel({
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
require.resolve('babel-plugin-template-html-minifier'),
|
|
||||||
{
|
|
||||||
modules: { lit: ['html', { name: 'css', encapsulation: 'style' }] },
|
|
||||||
failOnError: false,
|
|
||||||
strictCSS: true,
|
|
||||||
htmlMinifier: {
|
|
||||||
collapseWhitespace: true,
|
|
||||||
conservativeCollapse: true,
|
|
||||||
removeComments: true,
|
|
||||||
caseSensitive: true,
|
|
||||||
minifyCSS: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
/** Create and inject a service worker */
|
|
||||||
generateSW({
|
|
||||||
globIgnores: ['polyfills/*.js', 'nomodule-*.js'],
|
|
||||||
navigateFallback: '/index.html',
|
|
||||||
// where to output the generated sw
|
|
||||||
swDest: path.join('dist', 'sw.js'),
|
|
||||||
// directory to match patterns against to be precached
|
|
||||||
globDirectory: path.join('dist'),
|
|
||||||
// cache any html js and css by default
|
|
||||||
globPatterns: ['**/*.{html,js,css,webmanifest}'],
|
|
||||||
skipWaiting: true,
|
|
||||||
clientsClaim: true,
|
|
||||||
runtimeCaching: [{ urlPattern: 'polyfills/*.js', handler: 'CacheFirst' }],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,88 +0,0 @@
|
||||||
import { LitElement, html, css } from 'lit';
|
|
||||||
|
|
||||||
const logo = new URL('../assets/open-wc-logo.svg', import.meta.url).href;
|
|
||||||
|
|
||||||
class VyberClient extends LitElement {
|
|
||||||
static properties = {
|
|
||||||
header: { type: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
color: #1a2b42;
|
|
||||||
max-width: 960px;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: center;
|
|
||||||
background-color: var(--vyber-client-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin-top: 36px;
|
|
||||||
animation: app-logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes app-logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-footer {
|
|
||||||
font-size: calc(12px + 0.5vmin);
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-footer a {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.header = 'My app';
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return html`
|
|
||||||
<main>
|
|
||||||
<div class="logo"><img alt="open-wc logo" src=${logo} /></div>
|
|
||||||
<h1>${this.header}</h1>
|
|
||||||
|
|
||||||
<p>Edit <code>src/VyberClient.js</code> and save to reload.</p>
|
|
||||||
<a
|
|
||||||
class="app-link"
|
|
||||||
href="https://open-wc.org/guides/developing-components/code-examples/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Code examples
|
|
||||||
</a>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<p class="app-footer">
|
|
||||||
🚽 Made with love by
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href="https://github.com/open-wc"
|
|
||||||
>open-wc</a
|
|
||||||
>.
|
|
||||||
</p>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define('vyber-client', VyberClient);
|
|
|
@ -1,26 +0,0 @@
|
||||||
// import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
|
|
||||||
|
|
||||||
/** Use Hot Module replacement by adding --hmr to the start command */
|
|
||||||
const hmr = process.argv.includes('--hmr');
|
|
||||||
|
|
||||||
export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
|
|
||||||
open: '/',
|
|
||||||
watch: !hmr,
|
|
||||||
/** Resolve bare module imports */
|
|
||||||
nodeResolve: {
|
|
||||||
exportConditions: ['browser', 'development'],
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
|
|
||||||
// esbuildTarget: 'auto'
|
|
||||||
|
|
||||||
/** Set appIndex to enable SPA routing */
|
|
||||||
appIndex: './index.html',
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
/** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
|
|
||||||
// hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
|
|
||||||
],
|
|
||||||
|
|
||||||
// See documentation for all available options
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue