edit thread

main
jerl 2024-05-12 23:46:05 -07:00
parent e9f3b3f15a
commit 6531e692f7
6 changed files with 404 additions and 161 deletions

View File

@ -2,13 +2,34 @@ import { render, html } from '/uhtml.js';
import loadMessages from '/message.js';
import loadSpace from '/space.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) {
if (window.currentThread.id === this.thread.id)
return;
document.getElementById(`thread${window.currentThread.id}`)
.classList.remove('active');
let editform = document.getElementById('editthread');
if (editform) {
editform.remove();
edit.textContent = 'edit';
}
}
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;
@ -19,15 +40,8 @@ function chooseThread() {
document.getElementById('msginput').classList.add('hidden');
switchTab(document.getElementById(this.tab));
window.emit('get_thread', { thread: this.thread.id }, msg => {
window.currentThread.members = msg.thread.members;
document.getElementById('visibility').innerText = `${
msg.thread.permissions.everyone?.view.value === 'true' ?
'this thread is visible to everyone' :
'members can view this thread'}
${msg.thread.permissions.everyone?.post.value === 'true' ?
'anyone can post' :
msg.thread.permissions.members?.post.view ?
'only members can post' : 'select members can post'}`;
window.currentThread = msg.thread;
setVisibility();
document.getElementById('memberlist').replaceChildren(
...msg.thread.members.map(member =>
html.node`<p class='member'>${member.name}</p>`)
@ -48,48 +62,23 @@ function switchTab(tab) {
loadSpace();
}
function addMember() {
const name = document.getElementById('membername').value;
window.threadmembers.push(name);
document
.getElementById('newmembers')
.appendChild(html.node`<p class='member'>${name}</p>`);
document.getElementById('membername').value = '';
}
async function createThread(event) {
event.preventDefault();
let name = document.getElementById('newthreadname').value;
if (!name) {
document.getElementById('nameempty').classList.remove('hidden');
document.getElementById('newnameempty').classList.remove('hidden');
return;
}
let members = window.threadmembers.map(name => ({ name }));
const perms = document.querySelector(
'input[name="permissions"]:checked'
'input[name="newpermissions"]:checked'
).value;
if (perms === 'private_view')
members = (
await new Promise(resolve =>
window.emit('get_keys', { names: window.threadmembers }, resolve)
)
).keys;
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
};
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);
@ -100,18 +89,13 @@ async function createThread(event) {
const member = newmembers[i];
const sig = await openpgp.encrypt({
message: await openpgp.createMessage({ text: key }),
signingKeys: window.keys.priv,
signingKeys: window.keys.priv
});
}
*/
}
window.emit(
'create_thread',
{
name,
permissions,
members
},
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
@ -121,52 +105,125 @@ async function createThread(event) {
);
}
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('createseparator').remove();
document.getElementById('newthread').textContent = 'create';
} else {
window.threadmembers = [window.name];
document.getElementById('home')
.insertAdjacentElement('afterend', html.node`
<hr id='createseparator' class='separator' color='#505050'>`)
.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='nameempty' class='hidden'>name cannot be empty</p>
<input type='text' id='newthreadname' />
<p id='permissions'>thread permissions</p>
<input type='radio' id='public' name='permissions' value='public' checked />
<label for='public'>anyone can view and post</label><br />
<input type='radio' id='private_post'
name='permissions' value='private_post'
/>
<label for='private_post'>anyone can view, only members can post</label><br/>
<input type='radio' id='private_view'
name='permissions' value='private_view'
/>
<label for='private_view'>only members can view and post</label
><br /><br />
<label class='heading' for='membername'>members</label>
<input type='text' id='membername' placeholder='username' onkeydown=${event => {
if (event.key === 'Enter') {
event.preventDefault();
addMember();
}
}}/>
<button id='addmember' 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';
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>
<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) {
@ -188,6 +245,8 @@ document.body.append(html.node`
}>${window.name}</div>
</div>
<hr class='separator' color='#505050'>
<!-- create thread column goes here -->
<hr class='separator' color='#505050'>
<div id='profile' class='column hidden'>
<p><strong>authentication requests</strong></p>
<div id='authrequests'></div>
@ -196,7 +255,8 @@ document.body.append(html.node`
<div id='thread' class='column'>
<div id='content'>
<div id='title'>
thread: <strong id='threadname'>meow</strong>
<span>thread: <strong id='threadname'>meow</strong></span>
<button id='edit' class='hidden' onclick=${editThread}>edit</button>
</div>
<div id='buttons'>
<div id='tabs'>
@ -217,6 +277,7 @@ document.body.append(html.node`
<div id='memberlist'>
</div>
</div>
<hr class='separator' color='#505050'>
</div>
`);
@ -231,7 +292,18 @@ function makeThread(thread) {
return node;
}
window.socket.on('new_thread', thread => {
window.socket.on('thread', thread => {
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));
});

View File

@ -48,7 +48,7 @@
button,
input,
.tab {
padding: 5px 7px;
padding: 4px 7px;
}
input {
background: #1b1b1b;
@ -81,9 +81,11 @@
margin-bottom: 5px;
display: block;
}
h3,
h3 {
margin: 0;
}
h4 {
margin: 10px 0;
margin: 6px 0;
}
.hidden {
display: none !important;
@ -96,7 +98,8 @@
margin: 8px 2px;
}
.separator:has(+ .separator),
.separator:has(+ *.hidden) {
.separator:has(+ *.hidden),
.separator:last-child {
display: none;
}
#home {
@ -107,6 +110,12 @@
}
#threads {
margin: 3px;
min-height: 0;
display: flex;
flex-direction: column;
}
#threadlist {
overflow: auto;
}
#user {
padding: 6px;
@ -127,25 +136,26 @@
}
#newthread {
margin-top: 5px;
width: fit-content;
}
#createthread {
max-width: fit-content;
overflow: auto;
}
#permissions {
margin-bottom: 5px;
}
#thread {
display: flex;
margin: 0;
}
#content {
margin: 2px;
flex: 1;
display: flex;
flex-direction: column;
}
#title {
margin: 4px;
}
#buttons {
#title, #buttons {
margin: 4px 2px;
display: flex;
justify-content: space-between;
@ -194,8 +204,12 @@
margin: 4px;
}
}
#editthread {
max-width: fit-content;
}
#visibility {
white-space: pre-line;
margin-bottom: 12px;
}
.member {
margin: 5px 0;

View File

@ -7,31 +7,20 @@ const check_permission = async (user_id, thread_id) => {
[thread_id]
);
// check if the user is a member
const is_member =
(
await db.query('select * from member where thread = ? and user = ?', [
thread_id,
user_id,
])
const is_member = (
await db.query('select * from member where thread = ? and user = ?',
[thread_id, user_id])
).rows.length > 0;
const get_permission = (permission) => {
const relevant = permissions.rows.filter(
(i) => i.permission === permission
);
for (let i of relevant) {
if (i.type === 'everyone' && i.value === 'true') {
return true;
}
if (i.type === 'members' && i.value === 'true' && is_member) {
return true;
}
}
};
return {
is_member,
view: get_permission('view'),
post: get_permission('post')
};
let perms = { is_member };
for (let p of permissions.rows) {
if (p.type === 'everyone' && p.value === 'true')
perms[p.permission] = true;
else if (p.type === 'members' && is_member && p.value === 'true')
perms[p.permission] = true;
else if (p.type === 'user' && p.user === user_id && p.value === 'true')
perms[p.permission] = true;
}
return perms;
};
module.exports = check_permission;

View File

@ -1,7 +1,7 @@
const db = require('../db');
const authwrap = require('../authwrap');
const create_thread = async (msg, respond, socket) => {
const create_thread = async (msg, respond) => {
// validate inputs
if (typeof msg.name !== 'string') {
return respond({
@ -26,52 +26,61 @@ const create_thread = async (msg, respond, socket) => {
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, 'everyone', false, 'view', 'true']
[thread_id, 'everyone', true, 'view', 'true']
);
if (!msg.permissions || !msg.permissions.post_limited) {
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, 'everyone', false, 'post', 'true']
[thread_id, 'everyone', true, 'post', 'true']
);
} else {
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, 'members', false, 'post', 'true']
[thread_id, 'members', true, 'post', 'true']
);
}
} else {
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, 'members', false, 'view', 'true']
[thread_id, 'members', true, 'view', 'true']
);
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[thread_id, 'members', false, 'post', 'true']
[thread_id, 'members', true, 'post', 'true']
);
}
// add members
for (let user of msg.members) {
if (!user) continue;
// get user id
const id = await db.query('select id from user where name = ?', [
user.name,
]);
if (id.rows.length > 0) {
const user_id = id.rows[0].id;
if (Array.isArray(msg.members)) {
for (let member of msg.members) {
if (!member) continue;
// get user id
const id = await db.query('select id from user where name = ?', [
member.name,
]);
if (id.rows.length === 0) {
console.log('user not found: ' + member.name);
continue;
}
await db.query(
'insert into member (thread, user) values (?, ?)',
[thread_id, user_id]
[thread_id, id.rows[0].id]
);
if (typeof member.permissions === 'object')
for (let permission in member.permissions)
await db.query(`
insert into permission (thread, type, user, mutable, permission, value)
values (?, ?, ?, ?, ?, ?)`,
[thread_id, 'user', id.rows[0].id, true, permission, member.permissions[permission]]);
}
}
if (!msg.permissions || !msg.permissions.view_limited) {
for (let username in vybe.users) {
for (let socket of vybe.users[username].sockets) {
socket.emit('new_thread', {
socket.emit('thread', {
name: msg.name,
id: insert.rows[0].id,
permissions: {
@ -83,10 +92,12 @@ const create_thread = async (msg, respond, socket) => {
}
}
}
else {
else if (Array.isArray(msg.members)) {
for (let member of msg.members) {
if (!vybe.users[member.name])
continue;
for (let socket of vybe.users[member.name].sockets) {
socket.emit('new_thread', {
socket.emit('thread', {
name: msg.name,
id: insert.rows[0].id,
permissions: {
@ -101,7 +112,7 @@ const create_thread = async (msg, respond, socket) => {
// respond
return respond({
success: true,
id: insert.rows[0].id
id: thread_id
});
};

145
src/event/edit_thread.js Normal file
View File

@ -0,0 +1,145 @@
const db = require('../db');
const authwrap = require('../authwrap');
const check_permission = require('../check_permission');
const edit_thread = async (msg, respond) => {
// validate inputs
if (!msg.id || typeof msg.name !== 'string') {
return respond({
success: false,
message: 'invalid msg'
});
}
if (msg.name.length > 200) {
return respond({
success: false,
message: 'thread name 200 chars max'
});
}
const perms = await check_permission(msg.auth_user.id, msg.id);
if (!perms.admin) {
return respond({
success: false,
message: "user doesn't have permission"
});
}
// update name
await db.query(
'update thread set name = ? where id = ?',
[msg.name, msg.id]
);
// update permissions
let permissions = {};
for (const p of (await db.query(
`select type, permission, value, mutable
from permission
where type != 'user' and thread = ?`,
[msg.id]
)).rows) {
(permissions[p.type] || (permissions[p.type] = {}))
[p.permission] = {
value: p.value,
mutable: p.mutable
};
}
async function setPermission(type, permission, value) {
if (permissions[type] && permissions[type][permission]) {
if (!permissions[type][permission].mutable) {
respond({
success: false,
message: 'permission not mutable'
});
return false;
}
if (permissions[type][permission].value !== value) {
await db.query(`
update permission set value = ?
where thread = ? and type = ? and permission = ?`,
[value, msg.id, type, permission]
);
permissions[type][permission].value = value;
}
}
else {
await db.query(
`insert into permission (thread, type, mutable, permission, value)
values (?, ?, ?, ?, ?)`,
[msg.id, type, true, permission, value]
);
(permissions[type] || (permissions[type] = {}))
[permission] = {
value,
mutable: true
};
}
return true;
}
if (!msg.permissions || !msg.permissions.view_limited) {
if (!await setPermission('everyone', 'view', 'true'))
return;
if (!msg.permissions || !msg.permissions.post_limited) {
if (!await setPermission('everyone', 'post', 'true'))
return;
}
else {
if (!await setPermission('members', 'post', 'true'))
return;
if (!await setPermission('everyone', 'post', 'false'))
return;
}
} else {
if (!await setPermission('members', 'view', 'true'))
return;
if (!await setPermission('members', 'post', 'true'))
return;
if (!await setPermission('everyone', 'view', 'false'))
return;
if (!await setPermission('everyone', 'post', 'false'))
return;
}
if (!msg.permissions || !msg.permissions.view_limited) {
for (let username in vybe.users) {
for (let socket of vybe.users[username].sockets) {
socket.emit('thread', {
name: msg.name,
id: msg.id,
permissions: {
is_member: false,
view: true,
post: !msg.permissions || !msg.permissions.post_limited,
...permissions
}
});
}
}
}
else {
for (let member of (await db.query(
`select user.name from thread
join member on thread.id = member.thread
join user on user.id = member.user
where thread.id = ?`,
[msg.id]
)).rows) {
if (!vybe.users[member.name])
continue;
for (let socket of vybe.users[member.name].sockets) {
socket.emit('thread', {
name: msg.name,
id: msg.id,
permissions: {
is_member: true,
view: true,
post: true,
...permissions
}
});
}
}
}
return respond({
success: true
});
};
module.exports = authwrap(edit_thread);

View File

@ -9,7 +9,8 @@ const get_thread = async (msg, respond) => {
message: 'thread ID required'
});
}
if (!(await check_permission(msg.auth_user.id, msg.thread)).view) {
let perms = await check_permission(msg.auth_user.id, msg.thread);
if (!perms.view) {
return respond({
success: false,
message: "you can't view this thread"
@ -23,39 +24,50 @@ const get_thread = async (msg, respond) => {
[msg.thread]
);
const permissions = await db.query(
`select permission.type, permission.user, permission.permission, permission.value, permission.mutable
from thread
join permission on thread.id = permission.thread
where thread.id = ?`,
`select type, user, permission, value, mutable
from permission where thread = ?`,
[msg.thread]
);
let threadperms = {};
let members = Object.fromEntries(thread.rows.map(member => [member.id, member.user]));
let members = Object.fromEntries(thread.rows.map(member =>
[member.id, { name: member.user }]
));
for (let permission of permissions.rows) {
const member = members[permission.user];
if (member) {
if (!member.permissions)
member.permissions = {};
member.permissions[permission.permission] = {
value: permission.value,
mutable: permission.mutable
};
}
if (member)
(member.permissions || (member.permissions = {}))
[permission.permission] = {
value: permission.value,
mutable: permission.mutable
};
else
(threadperms[permission.type] || (threadperms[permission.type] = {}))
(perms[permission.type] || (perms[permission.type] = {}))
[permission.permission] = {
value: permission.value,
mutable: permission.mutable
};
}
function makeBool(type, permission) {
if (perms[type]) {
if (perms[type][permission])
perms[type][permission].value = perms[type][permission].value === 'true' ? true : false;
else
perms[type][permission] = { value: false, mutable: true };
} else
(perms[type] = {})[permission] = { value: false, mutable: true };
}
makeBool('everyone', 'view');
makeBool('everyone', 'post');
makeBool('members', 'view');
makeBool('members', 'post');
return respond({
success: true,
thread: {
id: msg.thread,
name: thread.rows[0].name,
permissions: threadperms,
permissions: perms,
members: Object.entries(members).map(member => ({
id: member[0],
name: member[1]
...member[1]
}))
}
});