mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-21 01:31:07 +00:00
feat(webapi): impl room deletion
This commit is contained in:
parent
9acf857781
commit
bc856f6c62
5 changed files with 205 additions and 25 deletions
|
@ -437,6 +437,12 @@ pub struct CreatePeerChat {
|
||||||
pub peer: PubKey,
|
pub peer: PubKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "typ", rename = "delete_room")]
|
||||||
|
pub struct DeleteRoomPayload {
|
||||||
|
pub room: Id,
|
||||||
|
}
|
||||||
|
|
||||||
/// A collection of room members, with these invariants:
|
/// A collection of room members, with these invariants:
|
||||||
/// 1. Sorted by userkeys.
|
/// 1. Sorted by userkeys.
|
||||||
/// 2. No duplicated users.
|
/// 2. No duplicated users.
|
||||||
|
@ -514,9 +520,10 @@ bitflags::bitflags! {
|
||||||
pub struct MemberPermission: u64 {
|
pub struct MemberPermission: u64 {
|
||||||
const POST_CHAT = 1 << 0;
|
const POST_CHAT = 1 << 0;
|
||||||
const ADD_MEMBER = 1 << 1;
|
const ADD_MEMBER = 1 << 1;
|
||||||
|
const DELETE_ROOM = 1 << 2;
|
||||||
|
|
||||||
const MAX_SELF_ADD = Self::POST_CHAT.bits();
|
const MAX_SELF_ADD = Self::POST_CHAT.bits();
|
||||||
const MAX_PEER_CHAT = Self::POST_CHAT.bits();
|
const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits();
|
||||||
|
|
||||||
const ALL = !0;
|
const ALL = !0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -517,6 +517,18 @@ pub trait TransactionOps {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn delete_room(&self, rid: Id) -> Result<bool> {
|
||||||
|
let deleted = prepare_cached_and_bind!(
|
||||||
|
self.conn(),
|
||||||
|
r"
|
||||||
|
DELETE FROM `room`
|
||||||
|
WHERE `rid` = :rid
|
||||||
|
"
|
||||||
|
)
|
||||||
|
.raw_execute()?;
|
||||||
|
Ok(deleted == 1)
|
||||||
|
}
|
||||||
|
|
||||||
fn add_room_member(&self, rid: Id, uid: i64, perm: MemberPermission) -> Result<()> {
|
fn add_room_member(&self, rid: Id, uid: i64, perm: MemberPermission) -> Result<()> {
|
||||||
let updated = prepare_cached_and_bind!(
|
let updated = prepare_cached_and_bind!(
|
||||||
self.conn(),
|
self.conn(),
|
||||||
|
|
|
@ -11,9 +11,10 @@ use axum::routing::{get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use axum_extra::extract::WithRejection as R;
|
use axum_extra::extract::WithRejection as R;
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission, RoomAdminOp,
|
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, Id,
|
||||||
RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, UserKey,
|
MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission,
|
||||||
UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY,
|
||||||
|
X_BLAH_NONCE,
|
||||||
};
|
};
|
||||||
use database::{Transaction, TransactionOps};
|
use database::{Transaction, TransactionOps};
|
||||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||||
|
@ -133,7 +134,7 @@ pub fn router(st: Arc<AppState>) -> Router {
|
||||||
.route("/user/me", get(user_get).post(user_register))
|
.route("/user/me", get(user_get).post(user_register))
|
||||||
.route("/room", get(room_list))
|
.route("/room", get(room_list))
|
||||||
.route("/room/create", post(room_create))
|
.route("/room/create", post(room_create))
|
||||||
.route("/room/:rid", get(room_get_metadata))
|
.route("/room/:rid", get(room_get_metadata).delete(room_delete))
|
||||||
// NB. Sync with `feed_url` and `next_url` generation.
|
// NB. Sync with `feed_url` and `next_url` generation.
|
||||||
.route("/room/:rid/feed.json", get(room_get_feed))
|
.route("/room/:rid/feed.json", get(room_get_feed))
|
||||||
.route("/room/:rid/msg", get(room_msg_list).post(room_msg_post))
|
.route("/room/:rid/msg", get(room_msg_list).post(room_msg_post))
|
||||||
|
@ -667,13 +668,47 @@ async fn room_join(
|
||||||
|
|
||||||
async fn room_leave(st: &AppState, rid: Id, user: &UserKey) -> Result<(), ApiError> {
|
async fn room_leave(st: &AppState, rid: Id, user: &UserKey) -> Result<(), ApiError> {
|
||||||
st.db.with_write(|txn| {
|
st.db.with_write(|txn| {
|
||||||
// FIXME: Handle peer chat room?
|
|
||||||
let (uid, ..) = txn.get_room_member(rid, user)?;
|
let (uid, ..) = txn.get_room_member(rid, user)?;
|
||||||
|
let (attrs, _) = txn.get_room_having(rid, RoomAttrs::empty())?;
|
||||||
|
if attrs.contains(RoomAttrs::PEER_CHAT) {
|
||||||
|
return Err(error_response!(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"invalid_operation",
|
||||||
|
"cannot leave a peer chat room without deleting it",
|
||||||
|
));
|
||||||
|
}
|
||||||
txn.remove_room_member(rid, uid)?;
|
txn.remove_room_member(rid, uid)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn room_delete(
|
||||||
|
st: ArcState,
|
||||||
|
R(Path(rid), _): RE<Path<Id>>,
|
||||||
|
SignedJson(op): SignedJson<DeleteRoomPayload>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
if rid != op.signee.payload.room {
|
||||||
|
return Err(error_response!(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"invalid_request",
|
||||||
|
"URI and payload room id mismatch",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
st.db.with_write(|txn| {
|
||||||
|
// TODO: Should we only shadow delete here?
|
||||||
|
let (_uid, perm, ..) = txn.get_room_member(rid, &op.signee.user)?;
|
||||||
|
if !perm.contains(MemberPermission::DELETE_ROOM) {
|
||||||
|
return Err(error_response!(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"permission_denied",
|
||||||
|
"the user does not have permission to delete the room",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
txn.delete_room(rid)?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn room_msg_mark_seen(
|
async fn room_msg_mark_seen(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
R(Path((rid, cid)), _): RE<Path<(Id, i64)>>,
|
R(Path((rid, cid)), _): RE<Path<(Id, i64)>>,
|
||||||
|
|
|
@ -11,10 +11,10 @@ use anyhow::Result;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission,
|
AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload,
|
||||||
PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission,
|
Id, MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
||||||
SignExt, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY,
|
ServerPermission, SignExt, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId,
|
||||||
X_BLAH_NONCE,
|
X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||||
};
|
};
|
||||||
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
@ -200,6 +200,31 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_peer_chat(
|
||||||
|
&self,
|
||||||
|
src: &User,
|
||||||
|
tgt: &User,
|
||||||
|
) -> impl Future<Output = Result<Id>> + use<'_> {
|
||||||
|
let req = self.sign(
|
||||||
|
src,
|
||||||
|
CreateRoomPayload::PeerChat(CreatePeerChat {
|
||||||
|
peer: tgt.pubkeys.id_key.clone(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
async move {
|
||||||
|
Ok(self
|
||||||
|
.request(Method::POST, "/room/create", None, Some(&req))
|
||||||
|
.await?
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_room(&self, rid: Id, user: &User) -> impl Future<Output = Result<()>> + use<'_> {
|
||||||
|
let req = self.sign(user, DeleteRoomPayload { room: rid });
|
||||||
|
self.request::<_, NoContent>(Method::DELETE, &format!("/room/{rid}"), None, Some(req))
|
||||||
|
.map_ok(|None| {})
|
||||||
|
}
|
||||||
|
|
||||||
fn join_room(
|
fn join_room(
|
||||||
&self,
|
&self,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
|
@ -718,28 +743,18 @@ async fn last_seen(server: Server) {
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn peer_chat(server: Server) {
|
async fn peer_chat(server: Server) {
|
||||||
let create_chat = |src: &User, tgt: &User| {
|
|
||||||
let req = server.sign(
|
|
||||||
src,
|
|
||||||
CreateRoomPayload::PeerChat(CreatePeerChat {
|
|
||||||
peer: tgt.pubkeys.id_key.clone(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
server
|
|
||||||
.request::<_, Id>(Method::POST, "/room/create", None, Some(req))
|
|
||||||
.map_ok(|resp| resp.unwrap())
|
|
||||||
};
|
|
||||||
|
|
||||||
// Bob disallows peer chat.
|
// Bob disallows peer chat.
|
||||||
create_chat(&ALICE, &BOB)
|
server
|
||||||
|
.create_peer_chat(&ALICE, &BOB)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "peer_user_not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "peer_user_not_found");
|
||||||
|
|
||||||
// Alice accepts bob.
|
// Alice accepts bob.
|
||||||
let rid = create_chat(&BOB, &ALICE).await.unwrap();
|
let rid = server.create_peer_chat(&BOB, &ALICE).await.unwrap();
|
||||||
|
|
||||||
// Room already exists.
|
// Room already exists.
|
||||||
create_chat(&BOB, &ALICE)
|
server
|
||||||
|
.create_peer_chat(&BOB, &ALICE)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::CONFLICT, "exists");
|
.expect_api_err(StatusCode::CONFLICT, "exists");
|
||||||
|
|
||||||
|
@ -789,6 +804,69 @@ async fn peer_chat(server: Server) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[case::group(false, true)]
|
||||||
|
#[case::peer_src(true, false)]
|
||||||
|
#[case::peer_tgt(true, true)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn delete_room(server: Server, #[case] peer_chat: bool, #[case] alice_delete: bool) {
|
||||||
|
let rid;
|
||||||
|
if peer_chat {
|
||||||
|
rid = server.create_peer_chat(&BOB, &ALICE).await.unwrap()
|
||||||
|
} else {
|
||||||
|
rid = server
|
||||||
|
.create_room(&ALICE, RoomAttrs::PUBLIC_JOINABLE, "public room")
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
server
|
||||||
|
.join_room(rid, &BOB, MemberPermission::MAX_SELF_ADD)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid rid.
|
||||||
|
server
|
||||||
|
.delete_room(Id::INVALID, &ALICE)
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found");
|
||||||
|
// Not in the room.
|
||||||
|
server
|
||||||
|
.delete_room(rid, &CAROL)
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found");
|
||||||
|
if !peer_chat {
|
||||||
|
// No permission.
|
||||||
|
server
|
||||||
|
.delete_room(rid, &BOB)
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not deleted yet.
|
||||||
|
server
|
||||||
|
.get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(&ALICE)))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// OK, deleted.
|
||||||
|
server
|
||||||
|
.delete_room(rid, if alice_delete { &ALICE } else { &BOB })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Should be deleted now.
|
||||||
|
server
|
||||||
|
.get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(&ALICE)))
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found");
|
||||||
|
|
||||||
|
// Peer found it deleted and cannot delete it again.
|
||||||
|
server
|
||||||
|
.delete_room(rid, if !alice_delete { &ALICE } else { &BOB })
|
||||||
|
.await
|
||||||
|
.expect_api_err(StatusCode::NOT_FOUND, "room_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn register(server: Server) {
|
async fn register(server: Server) {
|
||||||
|
|
|
@ -264,6 +264,26 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiError'
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
delete:
|
||||||
|
summary: Delete a room
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Signed-DeleteRoom'
|
||||||
|
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: Operation completed.
|
||||||
|
|
||||||
|
401:
|
||||||
|
description: Missing or invalid Authorization header.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
|
|
||||||
/_blah/room/{rid}/admin:
|
/_blah/room/{rid}/admin:
|
||||||
post:
|
post:
|
||||||
summary: Room management
|
summary: Room management
|
||||||
|
@ -712,6 +732,34 @@ components:
|
||||||
peer:
|
peer:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
Signed-DeleteRoom:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
sig:
|
||||||
|
type: string
|
||||||
|
signee:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
nonce:
|
||||||
|
type: integer
|
||||||
|
format: uint32
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
format: uint64
|
||||||
|
id_key:
|
||||||
|
type: string
|
||||||
|
act_key:
|
||||||
|
type: string
|
||||||
|
payload:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
typ:
|
||||||
|
type: string
|
||||||
|
const: 'delete_room'
|
||||||
|
room:
|
||||||
|
type: integer
|
||||||
|
format: in64
|
||||||
|
|
||||||
Signed-UserRegister:
|
Signed-UserRegister:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Add table
Reference in a new issue