mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-21 17:41:09 +00:00
feat: impl basic peer chat
This commit is contained in:
parent
9e96927693
commit
1e944ead31
7 changed files with 361 additions and 64 deletions
|
@ -28,7 +28,9 @@ impl fmt::Display for Id {
|
|||
}
|
||||
|
||||
impl Id {
|
||||
pub const INVALID: Self = Id(i64::MAX);
|
||||
pub const MIN: Self = Id(i64::MIN);
|
||||
pub const MAX: Self = Id(i64::MAX);
|
||||
pub const INVALID: Self = Self::MAX;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
@ -312,8 +314,8 @@ pub type ChatItem = WithSig<ChatPayload>;
|
|||
pub struct RoomMetadata {
|
||||
/// Room id.
|
||||
pub rid: Id,
|
||||
/// Plain text room title.
|
||||
pub title: String,
|
||||
/// Plain text room title. None for peer chat.
|
||||
pub title: Option<String>,
|
||||
/// Room attributes.
|
||||
pub attrs: RoomAttrs,
|
||||
|
||||
|
@ -333,11 +335,23 @@ pub struct RoomMetadata {
|
|||
/// Only available with authentication.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub member_permission: Option<MemberPermission>,
|
||||
/// The peer user, if this is a peer chat room.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub peer_user: Option<UserKey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "typ", rename = "create_room")]
|
||||
pub struct CreateRoomPayload {
|
||||
#[serde(tag = "typ")]
|
||||
pub enum CreateRoomPayload {
|
||||
#[serde(rename = "create_room")]
|
||||
Group(CreateGroup),
|
||||
#[serde(rename = "create_peer_chat")]
|
||||
PeerChat(CreatePeerChat),
|
||||
}
|
||||
|
||||
/// Multi-user room.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreateGroup {
|
||||
pub attrs: RoomAttrs,
|
||||
/// The initial member list. Besides invariants of `RoomMemberList`, this also must include the
|
||||
/// room creator themselves, with the highest permission (-1).
|
||||
|
@ -345,6 +359,12 @@ pub struct CreateRoomPayload {
|
|||
pub title: String,
|
||||
}
|
||||
|
||||
/// Peer-to-peer chat room with exactly two symmetric users.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreatePeerChat {
|
||||
pub peer: UserKey,
|
||||
}
|
||||
|
||||
/// A collection of room members, with these invariants:
|
||||
/// 1. Sorted by userkeys.
|
||||
/// 2. No duplicated users.
|
||||
|
@ -408,10 +428,13 @@ pub enum RoomAdminOp {
|
|||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// TODO: Is this a really all about permission, or is a generic `UserFlags`?
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ServerPermission: u64 {
|
||||
const CREATE_ROOM = 1 << 0;
|
||||
|
||||
const ACCEPT_PEER_CHAT = 1 << 16;
|
||||
|
||||
const ALL = !0;
|
||||
}
|
||||
|
||||
|
@ -421,6 +444,7 @@ bitflags::bitflags! {
|
|||
const ADD_MEMBER = 1 << 1;
|
||||
|
||||
const MAX_SELF_ADD = Self::POST_CHAT.bits();
|
||||
const MAX_PEER_CHAT = Self::POST_CHAT.bits();
|
||||
|
||||
const ALL = !0;
|
||||
}
|
||||
|
@ -430,6 +454,11 @@ bitflags::bitflags! {
|
|||
const PUBLIC_READABLE = 1 << 0;
|
||||
const PUBLIC_JOINABLE = 1 << 1;
|
||||
|
||||
const GROUP_ATTRS = (1 << 16) - 1;
|
||||
|
||||
// NB. Used by schema.
|
||||
const PEER_CHAT = 1 << 16;
|
||||
|
||||
const _ = !0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ use std::{fs, io};
|
|||
|
||||
use anyhow::{Context, Result};
|
||||
use blah_types::{
|
||||
bitflags, get_timestamp, ChatPayload, CreateRoomPayload, Id, MemberPermission, RichText,
|
||||
RoomAttrs, RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig,
|
||||
bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, MemberPermission,
|
||||
RichText, RoomAttrs, RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig,
|
||||
};
|
||||
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
||||
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey};
|
||||
|
@ -217,7 +217,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
|||
attrs,
|
||||
} => {
|
||||
let key = load_signing_key(&private_key_file)?;
|
||||
let payload = CreateRoomPayload {
|
||||
let payload = CreateRoomPayload::Group(CreateGroup {
|
||||
attrs: attrs.unwrap_or_default(),
|
||||
title,
|
||||
// The CLI does not support passing multiple members because `User` itself is a
|
||||
|
@ -226,7 +226,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
|||
permission: MemberPermission::ALL,
|
||||
user: UserKey(key.verifying_key().to_bytes()),
|
||||
}]),
|
||||
};
|
||||
});
|
||||
let payload = WithSig::sign(&key, get_timestamp(), &mut OsRng, payload)?;
|
||||
|
||||
let ret = client
|
||||
|
|
|
@ -7,12 +7,26 @@ CREATE TABLE IF NOT EXISTS `user` (
|
|||
`permission` INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
|
||||
-- The highest bit of `rid` will be set for peer chat room.
|
||||
-- So simply comparing it against 0 can filter them out.
|
||||
CREATE TABLE IF NOT EXISTS `room` (
|
||||
`rid` INTEGER NOT NULL PRIMARY KEY,
|
||||
`title` TEXT NOT NULL,
|
||||
-- RoomAttrs::PEER_CHAT
|
||||
`attrs` INTEGER NOT NULL
|
||||
CHECK ((`attrs` & 0x10000 == 0x10000) == `rid` < 0),
|
||||
`title` TEXT
|
||||
CHECK ((`title` ISNULL) == `rid` < 0),
|
||||
|
||||
`peer1` INTEGER REFERENCES `user` ON DELETE RESTRICT
|
||||
CHECK ((`peer1` NOTNULL) == `rid` < 0),
|
||||
`peer2` INTEGER REFERENCES `user` ON DELETE RESTRICT
|
||||
CHECK ((`peer2` NOTNULL AND `peer1` <= `peer2`) IS `rid` < 0)
|
||||
) STRICT;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS `ix_peer_chat` ON `room`
|
||||
(`peer1`, `peer2`)
|
||||
WHERE `rid` < 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_member` (
|
||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
||||
|
|
|
@ -7,6 +7,7 @@ use blah_types::Id;
|
|||
|
||||
pub trait IdExt {
|
||||
fn gen() -> Self;
|
||||
fn gen_peer_chat_rid() -> Self;
|
||||
}
|
||||
|
||||
impl IdExt for Id {
|
||||
|
@ -14,8 +15,15 @@ impl IdExt for Id {
|
|||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("after UNIX epoch");
|
||||
let timestamp_ms = timestamp.as_millis() as i64;
|
||||
assert!(timestamp_ms > 0);
|
||||
Id(timestamp_ms << 16)
|
||||
let timestamp_ms = timestamp.as_millis();
|
||||
assert!(
|
||||
0 < timestamp_ms && timestamp_ms < (1 << 48),
|
||||
"invalid timestamp",
|
||||
);
|
||||
Id((timestamp_ms as i64) << 16)
|
||||
}
|
||||
|
||||
fn gen_peer_chat_rid() -> Self {
|
||||
Id(Self::gen().0 | i64::MIN)
|
||||
}
|
||||
}
|
||||
|
|
181
blahd/src/lib.rs
181
blahd/src/lib.rs
|
@ -11,8 +11,9 @@ use axum::routing::{get, post};
|
|||
use axum::{Json, Router};
|
||||
use axum_extra::extract::WithRejection as R;
|
||||
use blah_types::{
|
||||
ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission, RoomAdminOp, RoomAdminPayload,
|
||||
RoomAttrs, RoomMetadata, ServerPermission, Signee, UserKey, WithItemId, WithSig,
|
||||
ChatItem, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission,
|
||||
RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signee, UserKey,
|
||||
WithItemId, WithSig,
|
||||
};
|
||||
use config::ServerConfig;
|
||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||
|
@ -177,7 +178,7 @@ async fn room_list(
|
|||
until_token: None,
|
||||
};
|
||||
let page_len = pagination.effective_page_len(&st);
|
||||
let start_rid = pagination.skip_token.unwrap_or(Id(0));
|
||||
let start_rid = pagination.skip_token.unwrap_or(Id::MIN);
|
||||
|
||||
let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> {
|
||||
let rooms = st
|
||||
|
@ -187,8 +188,6 @@ async fn room_list(
|
|||
.query_map(params, |row| {
|
||||
// TODO: Extract this into a function.
|
||||
let rid = row.get("rid")?;
|
||||
let title = row.get("title")?;
|
||||
let attrs = row.get("attrs")?;
|
||||
let last_item = row
|
||||
.get::<_, Option<Id>>("cid")?
|
||||
.map(|cid| {
|
||||
|
@ -209,18 +208,16 @@ async fn room_list(
|
|||
})
|
||||
})
|
||||
.transpose()?;
|
||||
let last_seen_cid =
|
||||
Some(row.get::<_, Id>("last_seen_cid")?).filter(|cid| cid.0 != 0);
|
||||
let unseen_cnt = row.get("unseen_cnt").ok();
|
||||
let member_permission = row.get("member_perm").ok();
|
||||
Ok(RoomMetadata {
|
||||
rid,
|
||||
title,
|
||||
attrs,
|
||||
title: row.get("title")?,
|
||||
attrs: row.get("attrs")?,
|
||||
last_item,
|
||||
last_seen_cid,
|
||||
unseen_cnt,
|
||||
member_permission,
|
||||
last_seen_cid: Some(row.get::<_, Id>("last_seen_cid")?)
|
||||
.filter(|cid| cid.0 != 0),
|
||||
unseen_cnt: row.get("unseen_cnt").ok(),
|
||||
member_permission: row.get("member_perm").ok(),
|
||||
peer_user: row.get("peer_userkey").ok(),
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
@ -255,12 +252,15 @@ async fn room_list(
|
|||
r"
|
||||
SELECT
|
||||
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
|
||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||
`peer_user`.`userkey` AS `peer_userkey`
|
||||
FROM `user`
|
||||
JOIN `room_member` USING (`uid`)
|
||||
JOIN `room` USING (`rid`)
|
||||
LEFT JOIN `room_item` USING (`rid`)
|
||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `room_item`.`uid`)
|
||||
LEFT JOIN `user` AS `peer_user` ON
|
||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
||||
WHERE `user`.`userkey` = :userkey AND
|
||||
`rid` > :start_rid
|
||||
GROUP BY `rid` HAVING `cid` IS MAX(`cid`)
|
||||
|
@ -281,6 +281,7 @@ async fn room_list(
|
|||
SELECT
|
||||
`rid`, `title`, `attrs`, `last_seen_cid`, `room_member`.`permission` AS `member_perm`,
|
||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||
`peer_user`.`userkey` AS `peer_userkey`,
|
||||
(SELECT COUNT(*)
|
||||
FROM `room_item` AS `unseen_item`
|
||||
WHERE `unseen_item`.`rid` = `room`.`rid` AND
|
||||
|
@ -290,6 +291,8 @@ async fn room_list(
|
|||
JOIN `room` USING (`rid`)
|
||||
LEFT JOIN `room_item` USING (`rid`)
|
||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `room_item`.`uid`)
|
||||
LEFT JOIN `user` AS `peer_user` ON
|
||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
||||
WHERE `user`.`userkey` = :userkey AND
|
||||
`rid` > :start_rid AND
|
||||
`cid` > `last_seen_cid`
|
||||
|
@ -312,10 +315,29 @@ async fn room_create(
|
|||
st: ArcState,
|
||||
SignedJson(params): SignedJson<CreateRoomPayload>,
|
||||
) -> Result<Json<Id>, ApiError> {
|
||||
let members = ¶ms.signee.payload.members.0;
|
||||
match params.signee.payload {
|
||||
CreateRoomPayload::Group(op) => room_create_group(&st, params.signee.user, op).await,
|
||||
CreateRoomPayload::PeerChat(op) => room_create_peer_chat(&st, params.signee.user, op).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn room_create_group(
|
||||
st: &AppState,
|
||||
user: UserKey,
|
||||
op: CreateGroup,
|
||||
) -> Result<Json<Id>, ApiError> {
|
||||
if !RoomAttrs::GROUP_ATTRS.contains(op.attrs) {
|
||||
return Err(error_response!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"deserialization",
|
||||
"invalid room attributes",
|
||||
));
|
||||
}
|
||||
|
||||
let members = &op.members.0;
|
||||
if !members
|
||||
.iter()
|
||||
.any(|m| m.user == params.signee.user && m.permission == MemberPermission::ALL)
|
||||
.any(|m| m.user == user && m.permission == MemberPermission::ALL)
|
||||
{
|
||||
return Err(error_response!(
|
||||
StatusCode::BAD_REQUEST,
|
||||
|
@ -332,7 +354,7 @@ async fn room_create(
|
|||
FROM `user`
|
||||
WHERE `userkey` = ?
|
||||
",
|
||||
params![params.signee.user],
|
||||
params![user],
|
||||
|row| {
|
||||
let perm = row.get::<_, ServerPermission>("permission")?;
|
||||
Ok(perm.contains(ServerPermission::CREATE_ROOM))
|
||||
|
@ -356,8 +378,8 @@ async fn room_create(
|
|||
",
|
||||
named_params! {
|
||||
":rid": rid,
|
||||
":title": params.signee.payload.title,
|
||||
":attrs": params.signee.payload.attrs,
|
||||
":title": op.title,
|
||||
":attrs": op.attrs,
|
||||
},
|
||||
)?;
|
||||
let mut insert_user = txn.prepare(
|
||||
|
@ -390,6 +412,118 @@ async fn room_create(
|
|||
Ok(Json(rid))
|
||||
}
|
||||
|
||||
async fn room_create_peer_chat(
|
||||
st: &AppState,
|
||||
src_user: UserKey,
|
||||
op: CreatePeerChat,
|
||||
) -> Result<Json<Id>, ApiError> {
|
||||
let tgt_user = op.peer;
|
||||
if tgt_user == src_user {
|
||||
return Err(error_response!(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not_implemented",
|
||||
"self-chat is not implemented yet",
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Access control and throttling.
|
||||
|
||||
let mut conn = st.db.get();
|
||||
let txn = conn.transaction()?;
|
||||
let tgt_uid = txn
|
||||
.query_row(
|
||||
r"
|
||||
SELECT `uid`
|
||||
FROM `user`
|
||||
WHERE `userkey` = :userkey AND
|
||||
`permission` & :perm = :perm
|
||||
",
|
||||
named_params! {
|
||||
":userkey": tgt_user,
|
||||
":perm": ServerPermission::ACCEPT_PEER_CHAT,
|
||||
},
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?
|
||||
.ok_or_else(|| {
|
||||
error_response!(
|
||||
StatusCode::NOT_FOUND,
|
||||
"not_found",
|
||||
"peer user does not exist or disallows peer chat",
|
||||
)
|
||||
})?;
|
||||
let src_uid = txn
|
||||
.query_row(
|
||||
r"
|
||||
SELECT `uid` FROM `user`
|
||||
WHERE `userkey` = ?
|
||||
",
|
||||
params![src_user],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)
|
||||
.optional()?;
|
||||
let src_uid = match src_uid {
|
||||
Some(uid) => uid,
|
||||
None => {
|
||||
txn.execute(
|
||||
r"
|
||||
INSERT INTO `user` (`userkey`)
|
||||
VALUES (?)
|
||||
",
|
||||
params![src_user],
|
||||
)?;
|
||||
txn.last_insert_rowid()
|
||||
}
|
||||
};
|
||||
|
||||
let (peer1, peer2) = if src_uid <= tgt_uid {
|
||||
(src_uid, tgt_uid)
|
||||
} else {
|
||||
(tgt_uid, src_uid)
|
||||
};
|
||||
let rid = Id::gen_peer_chat_rid();
|
||||
let updated = txn.execute(
|
||||
r"
|
||||
INSERT INTO `room` (`rid`, `attrs`, `peer1`, `peer2`)
|
||||
VALUES (:rid, :attrs, :peer1, :peer2)
|
||||
ON CONFLICT (`peer1`, `peer2`) WHERE `rid` < 0 DO NOTHING
|
||||
",
|
||||
named_params! {
|
||||
":rid": rid,
|
||||
":attrs": RoomAttrs::PEER_CHAT,
|
||||
":peer1": peer1,
|
||||
":peer2": peer2,
|
||||
},
|
||||
)?;
|
||||
if updated == 0 {
|
||||
return Err(error_response!(
|
||||
StatusCode::CONFLICT,
|
||||
"exists",
|
||||
"room already exists"
|
||||
));
|
||||
}
|
||||
|
||||
{
|
||||
let mut stmt = txn.prepare(
|
||||
r"
|
||||
INSERT INTO `room_member` (`rid`, `uid`, `permission`)
|
||||
VALUES (:rid, :uid, :perm)
|
||||
",
|
||||
)?;
|
||||
// TODO: Limit permission of the src user?
|
||||
for uid in [peer1, peer2] {
|
||||
stmt.execute(named_params! {
|
||||
":rid": rid,
|
||||
":uid": uid,
|
||||
":perm": MemberPermission::MAX_PEER_CHAT,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
txn.commit()?;
|
||||
Ok(Json(rid))
|
||||
}
|
||||
|
||||
/// Pagination query parameters.
|
||||
///
|
||||
/// Field names are inspired by Microsoft's design, which is an extension to OData spec.
|
||||
|
@ -444,7 +578,7 @@ async fn room_get_metadata(
|
|||
let conn = st.db.get();
|
||||
let (title, attrs) = get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |row| {
|
||||
Ok((
|
||||
row.get::<_, String>("title")?,
|
||||
row.get::<_, Option<String>>("title")?,
|
||||
row.get::<_, RoomAttrs>("attrs")?,
|
||||
))
|
||||
})?;
|
||||
|
@ -459,6 +593,7 @@ async fn room_get_metadata(
|
|||
last_seen_cid: None,
|
||||
unseen_cnt: None,
|
||||
member_permission: None,
|
||||
peer_user: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -624,8 +759,8 @@ fn query_room_items(
|
|||
.query_and_then(
|
||||
named_params! {
|
||||
":rid": rid,
|
||||
":after_cid": pagination.until_token.unwrap_or(Id(-1)),
|
||||
":before_cid": pagination.skip_token.unwrap_or(Id(i64::MAX)),
|
||||
":after_cid": pagination.until_token.unwrap_or(Id::MIN),
|
||||
":before_cid": pagination.skip_token.unwrap_or(Id::MAX),
|
||||
":limit": page_len,
|
||||
},
|
||||
|row| {
|
||||
|
|
|
@ -7,9 +7,9 @@ use std::sync::{Arc, LazyLock};
|
|||
|
||||
use anyhow::Result;
|
||||
use blah_types::{
|
||||
get_timestamp, AuthPayload, ChatItem, ChatPayload, CreateRoomPayload, Id, MemberPermission,
|
||||
RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMember, RoomMemberList, RoomMetadata,
|
||||
ServerPermission, UserKey, WithItemId, WithSig,
|
||||
get_timestamp, AuthPayload, ChatItem, ChatPayload, CreateGroup, CreatePeerChat,
|
||||
CreateRoomPayload, Id, MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs,
|
||||
RoomMember, RoomMemberList, RoomMetadata, ServerPermission, UserKey, WithItemId, WithSig,
|
||||
};
|
||||
use blahd::{ApiError, AppState, Database, RoomItems, RoomList};
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
@ -29,7 +29,7 @@ const LOCALHOST: &str = "127.0.0.1";
|
|||
static ALICE_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'A'; 32]));
|
||||
static ALICE: LazyLock<UserKey> = LazyLock::new(|| UserKey(ALICE_PRIV.verifying_key().to_bytes()));
|
||||
static BOB_PRIV: LazyLock<SigningKey> = LazyLock::new(|| SigningKey::from_bytes(&[b'B'; 32]));
|
||||
// static BOB: LazyLock<UserKey> = LazyLock::new(|| UserKey(BOB_PRIV.verifying_key().to_bytes()));
|
||||
static BOB: LazyLock<UserKey> = LazyLock::new(|| UserKey(BOB_PRIV.verifying_key().to_bytes()));
|
||||
|
||||
#[fixture]
|
||||
fn rng() -> impl RngCore {
|
||||
|
@ -118,14 +118,14 @@ impl Server {
|
|||
let req = sign(
|
||||
key,
|
||||
&mut *self.rng.borrow_mut(),
|
||||
CreateRoomPayload {
|
||||
CreateRoomPayload::Group(CreateGroup {
|
||||
attrs,
|
||||
members: RoomMemberList(vec![RoomMember {
|
||||
permission: MemberPermission::ALL,
|
||||
user: UserKey(key.verifying_key().to_bytes()),
|
||||
}]),
|
||||
title: title.to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
async move {
|
||||
Ok(self
|
||||
|
@ -254,9 +254,10 @@ fn auth(key: &SigningKey, rng: &mut impl RngCore) -> String {
|
|||
#[case::private(false)]
|
||||
#[tokio::test]
|
||||
async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] public: bool) {
|
||||
let title = "test room";
|
||||
let mut room_meta = RoomMetadata {
|
||||
rid: Id(0),
|
||||
title: "test room".into(),
|
||||
title: Some(title.into()),
|
||||
attrs: if public {
|
||||
RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE
|
||||
} else {
|
||||
|
@ -266,18 +267,19 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
|
|||
last_seen_cid: None,
|
||||
unseen_cnt: None,
|
||||
member_permission: None,
|
||||
peer_user: None,
|
||||
};
|
||||
|
||||
// Alice has permission.
|
||||
let rid = server
|
||||
.create_room(&ALICE_PRIV, room_meta.attrs, &room_meta.title)
|
||||
.create_room(&ALICE_PRIV, room_meta.attrs, title)
|
||||
.await
|
||||
.unwrap();
|
||||
room_meta.rid = rid;
|
||||
|
||||
// Bob has no permission.
|
||||
server
|
||||
.create_room(&BOB_PRIV, room_meta.attrs, &room_meta.title)
|
||||
.create_room(&BOB_PRIV, room_meta.attrs, title)
|
||||
.await
|
||||
.expect_api_err(StatusCode::FORBIDDEN, "permission_denied");
|
||||
|
||||
|
@ -579,12 +581,13 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
|||
RoomList {
|
||||
rooms: vec![RoomMetadata {
|
||||
rid,
|
||||
title: title.into(),
|
||||
title: Some(title.into()),
|
||||
attrs,
|
||||
last_item: Some(alice_chat2.clone()),
|
||||
last_seen_cid: None,
|
||||
unseen_cnt: Some(2),
|
||||
member_permission: Some(member_perm),
|
||||
peer_user: None,
|
||||
}],
|
||||
skip_token: None,
|
||||
}
|
||||
|
@ -612,12 +615,13 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
|||
RoomList {
|
||||
rooms: vec![RoomMetadata {
|
||||
rid,
|
||||
title: title.into(),
|
||||
title: Some(title.into()),
|
||||
attrs,
|
||||
last_item: Some(alice_chat2.clone()),
|
||||
last_seen_cid: Some(alice_chat1.cid),
|
||||
unseen_cnt: Some(1),
|
||||
member_permission: Some(member_perm),
|
||||
peer_user: None,
|
||||
}],
|
||||
skip_token: None,
|
||||
}
|
||||
|
@ -639,3 +643,76 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
|||
.unwrap();
|
||||
assert_eq!(rooms, RoomList::default());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||
let mut create_chat = |src: &SigningKey, tgt: &UserKey| {
|
||||
let req = sign(
|
||||
src,
|
||||
rng,
|
||||
CreateRoomPayload::PeerChat(CreatePeerChat { peer: tgt.clone() }),
|
||||
);
|
||||
server
|
||||
.request::<_, Id>(Method::POST, "/room/create", None, Some(req))
|
||||
.map_ok(|resp| resp.unwrap())
|
||||
};
|
||||
|
||||
// Bob disallows peer chat.
|
||||
create_chat(&ALICE_PRIV, &BOB)
|
||||
.await
|
||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||
|
||||
// Alice accepts bob.
|
||||
let rid = create_chat(&BOB_PRIV, &ALICE).await.unwrap();
|
||||
|
||||
// Room already exists.
|
||||
create_chat(&BOB_PRIV, &ALICE)
|
||||
.await
|
||||
.expect_api_err(StatusCode::CONFLICT, "exists");
|
||||
|
||||
// Peer chat room is not public.
|
||||
let rooms = server
|
||||
.get::<RoomList>("/room?filter=public", None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rooms, RoomList::default());
|
||||
server
|
||||
.get::<RoomMetadata>(&format!("/room/{rid}"), None)
|
||||
.await
|
||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||
|
||||
// Both alice and bob are in the room.
|
||||
for (key, peer) in [(&*ALICE_PRIV, &*BOB), (&*BOB_PRIV, &*ALICE)] {
|
||||
let mut expect_meta = RoomMetadata {
|
||||
rid,
|
||||
title: None,
|
||||
attrs: RoomAttrs::PEER_CHAT,
|
||||
last_item: None,
|
||||
last_seen_cid: None,
|
||||
unseen_cnt: None,
|
||||
member_permission: None,
|
||||
peer_user: None,
|
||||
};
|
||||
|
||||
let meta = server
|
||||
.get::<RoomMetadata>(&format!("/room/{rid}"), Some(&auth(key, rng)))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(meta, expect_meta);
|
||||
|
||||
expect_meta.member_permission = Some(MemberPermission::MAX_PEER_CHAT);
|
||||
expect_meta.peer_user = Some(peer.clone());
|
||||
let rooms = server
|
||||
.get::<RoomList>("/room?filter=joined", Some(&auth(key, rng)))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
rooms,
|
||||
RoomList {
|
||||
rooms: vec![expect_meta],
|
||||
skip_token: None
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,6 +108,13 @@ paths:
|
|||
/room/create:
|
||||
post:
|
||||
summary: Create room
|
||||
|
||||
description:
|
||||
When `typ="create_room"`, create a multi-user room.
|
||||
|
||||
When `typ="create_peer_chat"`, create a peer-to-peer room between two
|
||||
users. There can be at most one peer room for each given user pair.
|
||||
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
@ -130,6 +137,20 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
404:
|
||||
description: The peer user does not exist or disallows peer chat.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
409:
|
||||
description: There is already a peer chat room between the user pair.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
/room/{rid}:
|
||||
get:
|
||||
summary: Get room metadata
|
||||
|
@ -391,6 +412,10 @@ components:
|
|||
member_permission:
|
||||
type: integer
|
||||
format: int64
|
||||
peer_user:
|
||||
type: string
|
||||
description: |
|
||||
For peer chat room, this gives the identity of the peer user.
|
||||
|
||||
RoomMetadata:
|
||||
type: object
|
||||
|
@ -577,23 +602,32 @@ components:
|
|||
type: integer
|
||||
format: uint32
|
||||
payload:
|
||||
type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'create_room'
|
||||
title:
|
||||
type: string
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
user:
|
||||
type: string
|
||||
permission:
|
||||
type: integer
|
||||
format: int64
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'create_room'
|
||||
title:
|
||||
type: string
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
user:
|
||||
type: string
|
||||
permission:
|
||||
type: integer
|
||||
format: int64
|
||||
- type: object
|
||||
properties:
|
||||
typ:
|
||||
type: string
|
||||
const: 'create_peer_chat'
|
||||
peer:
|
||||
type: string
|
||||
|
||||
example:
|
||||
sig: 99a77e836538268839ed3419c649eefb043cb51d448f641cc2a1c523811aab4aacd09f92e7c0688ffd659bfc6acb764fea79979a491e132bf6a56dd23adc1d09
|
||||
signee:
|
||||
|
|
Loading…
Add table
Reference in a new issue