Impl room leaving and fix frontend room combobox

This commit is contained in:
oxalica 2024-09-03 05:38:20 -04:00
parent cc51d53575
commit 2eb884766a
4 changed files with 159 additions and 40 deletions

View file

@ -12,8 +12,8 @@ use axum::routing::{get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use axum_extra::extract::WithRejection; use axum_extra::extract::WithRejection;
use blah::types::{ use blah::types::{
ChatItem, ChatPayload, CreateRoomPayload, MemberPermission, RoomAdminPayload, RoomAttrs, ChatItem, ChatPayload, CreateRoomPayload, MemberPermission, RoomAdminOp, RoomAdminPayload,
ServerPermission, Signee, UserKey, WithSig, RoomAttrs, ServerPermission, Signee, UserKey, WithSig,
}; };
use config::Config; use config::Config;
use ed25519_dalek::SIGNATURE_LENGTH; use ed25519_dalek::SIGNATURE_LENGTH;
@ -737,7 +737,7 @@ async fn room_admin(
Path(ruuid): Path<Uuid>, Path(ruuid): Path<Uuid>,
SignedJson(op): SignedJson<RoomAdminPayload>, SignedJson(op): SignedJson<RoomAdminPayload>,
) -> Result<StatusCode, ApiError> { ) -> Result<StatusCode, ApiError> {
if ruuid != *op.signee.payload.room() { if ruuid != op.signee.payload.room {
return Err(error_response!( return Err(error_response!(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
"invalid_request", "invalid_request",
@ -745,24 +745,45 @@ async fn room_admin(
)); ));
} }
let RoomAdminPayload::AddMember { match op.signee.payload.op {
permission, user, .. RoomAdminOp::AddMember { user, permission } => {
} = op.signee.payload; if user != op.signee.user {
if user != op.signee.user { return Err(error_response!(
return Err(error_response!( StatusCode::NOT_IMPLEMENTED,
StatusCode::NOT_IMPLEMENTED, "not_implemented",
"not_implemented", "only self-adding is implemented yet",
"only self-adding is implemented yet", ));
)); }
} if permission.is_empty() || !MemberPermission::MAX_SELF_ADD.contains(permission) {
if permission.is_empty() || !MemberPermission::MAX_SELF_ADD.contains(permission) { return Err(error_response!(
return Err(error_response!( StatusCode::BAD_REQUEST,
StatusCode::BAD_REQUEST, "deserialization",
"deserialization", "invalid permission",
"invalid permission", ));
)); }
room_join(&st, ruuid, user, permission).await?;
}
RoomAdminOp::RemoveMember { user } => {
if user != op.signee.user {
return Err(error_response!(
StatusCode::NOT_IMPLEMENTED,
"not_implemented",
"only self-removing is implemented yet",
));
}
room_leave(&st, ruuid, user).await?;
}
} }
Ok(StatusCode::NO_CONTENT)
}
async fn room_join(
st: &AppState,
ruuid: Uuid,
user: UserKey,
permission: MemberPermission,
) -> Result<(), ApiError> {
let mut conn = st.conn.lock().unwrap(); let mut conn = st.conn.lock().unwrap();
let txn = conn.transaction()?; let txn = conn.transaction()?;
let Some(rid) = txn let Some(rid) = txn
@ -811,6 +832,49 @@ async fn room_admin(
}, },
)?; )?;
txn.commit()?; txn.commit()?;
Ok(())
Ok(StatusCode::NO_CONTENT) }
async fn room_leave(st: &AppState, ruuid: Uuid, user: UserKey) -> Result<(), ApiError> {
let mut conn = st.conn.lock().unwrap();
let txn = conn.transaction()?;
let Some((rid, uid)) = txn
.query_row(
r"
SELECT `rid`, `uid`
FROM `room_member`
JOIN `room` USING (`rid`)
JOIN `user` USING (`uid`)
WHERE `ruuid` = :ruuid AND
`userkey` = :userkey
",
named_params! {
":ruuid": ruuid,
":userkey": user,
},
|row| Ok((row.get::<_, u64>("rid")?, row.get::<_, u64>("uid")?)),
)
.optional()?
else {
return Err(error_response!(
StatusCode::NOT_FOUND,
"not_found",
"room does not exists or user is not a room member",
));
};
txn.execute(
r"
DELETE FROM `room_member`
WHERE `rid` = :rid AND
`uid` = :uid
",
named_params! {
":rid": rid,
":uid": uid,
},
)?;
txn.commit()?;
Ok(())
} }

View file

@ -71,6 +71,7 @@
<label for="join-new-room">join public room:</label> <label for="join-new-room">join public room:</label>
<select id="join-new-room"></select> <select id="join-new-room"></select>
<button id="leave-room">leave room</select>
<button id="refresh-rooms">refresh room list</select> <button id="refresh-rooms">refresh room list</select>
</div> </div>
<div> <div>

View file

@ -2,6 +2,7 @@ const msgFlow = document.querySelector('#msg-flow');
const userPubkeyDisplay = document.querySelector('#user-pubkey'); const userPubkeyDisplay = document.querySelector('#user-pubkey');
const serverUrlInput = document.querySelector('#server-url'); const serverUrlInput = document.querySelector('#server-url');
const roomsInput = document.querySelector('#rooms'); const roomsInput = document.querySelector('#rooms');
const leaveRoomBtn = document.querySelector('#leave-room');
const joinNewRoomInput = document.querySelector('#join-new-room'); const joinNewRoomInput = document.querySelector('#join-new-room');
const chatInput = document.querySelector('#chat'); const chatInput = document.querySelector('#chat');
const regenKeyBtn = document.querySelector('#regen-key'); const regenKeyBtn = document.querySelector('#regen-key');
@ -172,6 +173,7 @@ async function genAuthHeader() {
async function enterRoom(ruuid) { async function enterRoom(ruuid) {
log(`loading room: ${ruuid}`); log(`loading room: ${ruuid}`);
curRoom = ruuid; curRoom = ruuid;
roomsInput.value = ruuid;
genAuthHeader() genAuthHeader()
.then(opts => fetch(`${serverUrl}/room/${ruuid}`, opts)) .then(opts => fetch(`${serverUrl}/room/${ruuid}`, opts))
@ -218,7 +220,7 @@ async function connectServer(newServerUrl) {
log('connecting server'); log('connecting server');
wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:'; wsUrl.protocol = wsUrl.protocol == 'http:' ? 'ws:' : 'wss:';
wsUrl.pathname += '/ws'; wsUrl.pathname += wsUrl.pathname.endsWith('/') ? 'ws' : '/ws';
ws = new WebSocket(wsUrl); ws = new WebSocket(wsUrl);
ws.onopen = async (_) => { ws.onopen = async (_) => {
const auth = await signData({ typ: 'auth' }); const auth = await signData({ typ: 'auth' });
@ -256,8 +258,14 @@ async function loadRoomList(autoJoin) {
log('loading room list'); log('loading room list');
async function loadInto(targetEl, filter) { async function loadInto(targetEl, filter) {
const emptyEl = document.createElement('option');
emptyEl.value = '';
emptyEl.innerText = '-';
emptyEl.disabled = true;
targetEl.replaceChildren(emptyEl);
targetEl.value = '';
try { try {
targetEl.replaceChildren();
const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader()) const resp = await fetch(`${serverUrl}/room?filter=${filter}`, await genAuthHeader())
const json = await resp.json() const json = await resp.json()
if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`); if (resp.status !== 200) throw new Error(`status ${resp.status}: ${json.error.message}`);
@ -274,8 +282,11 @@ async function loadRoomList(autoJoin) {
loadInto(roomsInput, 'joined') loadInto(roomsInput, 'joined')
.then(async (_) => { .then(async (_) => {
if (autoJoin && roomsInput.value !== '') { if (autoJoin) {
await enterRoom(roomsInput.value); const el = roomsInput.querySelector('option:nth-child(2)');
if (el !== null) {
await enterRoom(el.value);
}
} }
}); });
@ -303,6 +314,25 @@ async function joinRoom(ruuid) {
} }
} }
async function leaveRoom() {
try {
leaveRoomBtn.disabled = true;
await signAndPost(`${serverUrl}/room/${curRoom}/admin`, {
room: curRoom,
typ: 'remove_member',
user: await getUserPubkey(),
});
log('left room');
await loadRoomList(true);
} catch (e) {
console.error(e);
log(`failed to leave room: ${e}`);
} finally {
leaveRoomBtn.disabled = false;
}
}
async function signAndPost(url, data) { async function signAndPost(url, data) {
const signedPayload = await signData(data); const signedPayload = await signData(data);
const resp = await fetch(url, { const resp = await fetch(url, {
@ -398,6 +428,9 @@ chatInput.onkeypress = async (e) => {
regenKeyBtn.onclick = async (_) => { regenKeyBtn.onclick = async (_) => {
await generateKeypair(); await generateKeypair();
}; };
leaveRoomBtn.onclick = async (_) => {
await leaveRoom();
};
roomsInput.onchange = async (_) => { roomsInput.onchange = async (_) => {
await enterRoom(roomsInput.value); await enterRoom(roomsInput.value);
}; };

View file

@ -326,23 +326,25 @@ pub struct RoomMember {
#[serde(tag = "typ", rename = "auth")] #[serde(tag = "typ", rename = "auth")]
pub struct AuthPayload {} pub struct AuthPayload {}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "typ", rename_all = "snake_case")] #[serde(tag = "typ", rename_all = "snake_case")]
pub enum RoomAdminPayload { pub struct RoomAdminPayload {
AddMember { pub room: Uuid,
permission: MemberPermission, #[serde(flatten)]
room: Uuid, pub op: RoomAdminOp,
user: UserKey,
},
// TODO: CRUD
} }
impl RoomAdminPayload { #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub fn room(&self) -> &Uuid { #[serde(tag = "typ", rename_all = "snake_case")]
match self { pub enum RoomAdminOp {
RoomAdminPayload::AddMember { room, .. } => room, AddMember {
} permission: MemberPermission,
} user: UserKey,
},
RemoveMember {
user: UserKey,
},
// TODO: RU
} }
bitflags! { bitflags! {
@ -497,4 +499,23 @@ mod tests {
let got = serde_json::to_string(&text).unwrap(); let got = serde_json::to_string(&text).unwrap();
assert_eq!(got, raw); assert_eq!(got, raw);
} }
#[test]
fn room_admin_serde() {
let data = RoomAdminPayload {
room: Uuid::nil(),
op: RoomAdminOp::AddMember {
permission: MemberPermission::POST_CHAT,
user: UserKey([0x42; PUBLIC_KEY_LENGTH]),
},
};
let raw = serde_jcs::to_string(&data).unwrap();
assert_eq!(
raw,
r#"{"permission":1,"room":"00000000-0000-0000-0000-000000000000","typ":"add_member","user":"4242424242424242424242424242424242424242424242424242424242424242"}"#
);
let got = serde_json::from_str::<RoomAdminPayload>(&raw).unwrap();
assert_eq!(got, data);
}
} }