mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-20 17:21:08 +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,
|
||||
}
|
||||
|
||||
#[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:
|
||||
/// 1. Sorted by userkeys.
|
||||
/// 2. No duplicated users.
|
||||
|
@ -514,9 +520,10 @@ bitflags::bitflags! {
|
|||
pub struct MemberPermission: u64 {
|
||||
const POST_CHAT = 1 << 0;
|
||||
const ADD_MEMBER = 1 << 1;
|
||||
const DELETE_ROOM = 1 << 2;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -517,6 +517,18 @@ pub trait TransactionOps {
|
|||
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<()> {
|
||||
let updated = prepare_cached_and_bind!(
|
||||
self.conn(),
|
||||
|
|
|
@ -11,9 +11,10 @@ use axum::routing::{get, post};
|
|||
use axum::{Json, Router};
|
||||
use axum_extra::extract::WithRejection as R;
|
||||
use blah_types::{
|
||||
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission, RoomAdminOp,
|
||||
RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signed, SignedChatMsg, UserKey,
|
||||
UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, Id,
|
||||
MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission,
|
||||
Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY,
|
||||
X_BLAH_NONCE,
|
||||
};
|
||||
use database::{Transaction, TransactionOps};
|
||||
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("/room", get(room_list))
|
||||
.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.
|
||||
.route("/room/:rid/feed.json", get(room_get_feed))
|
||||
.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> {
|
||||
st.db.with_write(|txn| {
|
||||
// FIXME: Handle peer chat room?
|
||||
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)?;
|
||||
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(
|
||||
st: ArcState,
|
||||
R(Path((rid, cid)), _): RE<Path<(Id, i64)>>,
|
||||
|
|
|
@ -11,10 +11,10 @@ use anyhow::Result;
|
|||
use axum::http::HeaderMap;
|
||||
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
||||
use blah_types::{
|
||||
AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission,
|
||||
PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission,
|
||||
SignExt, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY,
|
||||
X_BLAH_NONCE,
|
||||
AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload,
|
||||
Id, MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
||||
ServerPermission, SignExt, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId,
|
||||
X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||
};
|
||||
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
||||
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(
|
||||
&self,
|
||||
rid: Id,
|
||||
|
@ -718,28 +743,18 @@ async fn last_seen(server: Server) {
|
|||
#[rstest]
|
||||
#[tokio::test]
|
||||
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.
|
||||
create_chat(&ALICE, &BOB)
|
||||
server
|
||||
.create_peer_chat(&ALICE, &BOB)
|
||||
.await
|
||||
.expect_api_err(StatusCode::NOT_FOUND, "peer_user_not_found");
|
||||
|
||||
// Alice accepts bob.
|
||||
let rid = create_chat(&BOB, &ALICE).await.unwrap();
|
||||
let rid = server.create_peer_chat(&BOB, &ALICE).await.unwrap();
|
||||
|
||||
// Room already exists.
|
||||
create_chat(&BOB, &ALICE)
|
||||
server
|
||||
.create_peer_chat(&BOB, &ALICE)
|
||||
.await
|
||||
.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]
|
||||
#[tokio::test]
|
||||
async fn register(server: Server) {
|
||||
|
|
|
@ -264,6 +264,26 @@ paths:
|
|||
schema:
|
||||
$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:
|
||||
post:
|
||||
summary: Room management
|
||||
|
@ -712,6 +732,34 @@ components:
|
|||
peer:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
|
Loading…
Add table
Reference in a new issue