mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
refactor(*): use term msg
to replace item
- `Msg` or `msg` is now the canonical term for the substructure in a room. It includes a `chat` subtype and (in the future) other administration subtypes like member joining or leaving. - `Message` or `message` can used in human oriented context like docs and comments, but only when it is unambiguous. - `message` is not chosen in code because it's hard to type (at least for me!), and have ambiguous meaning of: - "Human readable text" in context of `ApiError`'s field. - "A unit of data transfer, datagram" in context of WebSocket Message. - `item` is not chosen because it is overly generic.
This commit is contained in:
parent
4acc103afa
commit
73eb441a26
8 changed files with 171 additions and 175 deletions
|
@ -14,7 +14,7 @@ use serde_with::{serde_as, DisplayFromStr};
|
||||||
pub use bitflags;
|
pub use bitflags;
|
||||||
pub use ed25519_dalek;
|
pub use ed25519_dalek;
|
||||||
|
|
||||||
/// An opaque server-specific ID for room, chat item, and etc.
|
/// An opaque server-specific ID for rooms, messages, and etc.
|
||||||
/// It's currently serialized as a string for JavaScript's convenience.
|
/// It's currently serialized as a string for JavaScript's convenience.
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
@ -34,15 +34,15 @@ impl Id {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct WithItemId<T> {
|
pub struct WithMsgId<T> {
|
||||||
pub cid: Id,
|
pub cid: Id,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub item: T,
|
pub msg: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> WithItemId<T> {
|
impl<T> WithMsgId<T> {
|
||||||
pub fn new(cid: Id, item: T) -> Self {
|
pub fn new(cid: Id, msg: T) -> Self {
|
||||||
Self { cid, item }
|
Self { cid, msg }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -308,7 +308,7 @@ impl RichText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ChatItem = WithSig<ChatPayload>;
|
pub type SignedChatMsg = WithSig<ChatPayload>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RoomMetadata {
|
pub struct RoomMetadata {
|
||||||
|
@ -320,14 +320,14 @@ pub struct RoomMetadata {
|
||||||
pub attrs: RoomAttrs,
|
pub attrs: RoomAttrs,
|
||||||
|
|
||||||
// Extra information is only available for some APIs.
|
// Extra information is only available for some APIs.
|
||||||
/// The last item in the room.
|
/// The last message in the room.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub last_item: Option<WithItemId<ChatItem>>,
|
pub last_msg: Option<WithMsgId<SignedChatMsg>>,
|
||||||
/// The current user's last seen item id.
|
/// The current user's last seen message's `cid`.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub last_seen_cid: Option<Id>,
|
pub last_seen_cid: Option<Id>,
|
||||||
/// The number of unseen messages, ie. the number of items from `last_seen_cid` to
|
/// The number of unseen messages, ie. the number of messages from `last_seen_cid` to
|
||||||
/// `last_item.cid`.
|
/// `last_msg.cid`.
|
||||||
/// This may or may not be a precise number.
|
/// This may or may not be a precise number.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub unseen_cnt: Option<u64>,
|
pub unseen_cnt: Option<u64>,
|
||||||
|
@ -547,11 +547,11 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonical_chat() {
|
fn canonical_msg() {
|
||||||
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
||||||
let signing_key = SigningKey::from_bytes(&[0x42; 32]);
|
let signing_key = SigningKey::from_bytes(&[0x42; 32]);
|
||||||
let timestamp = 0xDEAD_BEEF;
|
let timestamp = 0xDEAD_BEEF;
|
||||||
let item = WithSig::sign(
|
let msg = WithSig::sign(
|
||||||
&signing_key,
|
&signing_key,
|
||||||
timestamp,
|
timestamp,
|
||||||
&mut fake_rng,
|
&mut fake_rng,
|
||||||
|
@ -562,15 +562,15 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let json = serde_jcs::to_string(&item).unwrap();
|
let json = serde_jcs::to_string(&msg).unwrap();
|
||||||
let expect = expect![[
|
let expect = expect![[
|
||||||
r#"{"sig":"18ee190722bebfd438c82f34890540d91578b4ba9f6c0c6011cc4fd751a321e32e9442d00dad1920799c54db011694c72a9ba993b408922e9997119209aa5e09","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"#
|
r#"{"sig":"18ee190722bebfd438c82f34890540d91578b4ba9f6c0c6011cc4fd751a321e32e9442d00dad1920799c54db011694c72a9ba993b408922e9997119209aa5e09","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"#
|
||||||
]];
|
]];
|
||||||
expect.assert_eq(&json);
|
expect.assert_eq(&json);
|
||||||
|
|
||||||
let roundtrip_item = serde_json::from_str::<WithSig<ChatPayload>>(&json).unwrap();
|
let roundtrip_msg = serde_json::from_str::<WithSig<ChatPayload>>(&json).unwrap();
|
||||||
assert_eq!(roundtrip_item, item);
|
assert_eq!(roundtrip_msg, msg);
|
||||||
roundtrip_item.verify().unwrap();
|
roundtrip_msg.verify().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS `room_member` (
|
||||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||||
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
||||||
`permission` INTEGER NOT NULL,
|
`permission` INTEGER NOT NULL,
|
||||||
-- Optionally references `room_item`(`cid`).
|
-- Optionally references `msg`(`cid`).
|
||||||
`last_seen_cid` INTEGER NOT NULL DEFAULT 0,
|
`last_seen_cid` INTEGER NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (`rid`, `uid`)
|
PRIMARY KEY (`rid`, `uid`)
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS `room_member` (
|
||||||
CREATE INDEX IF NOT EXISTS `ix_member_room` ON `room_member`
|
CREATE INDEX IF NOT EXISTS `ix_member_room` ON `room_member`
|
||||||
(`uid` ASC, `rid` ASC, `permission`, `last_seen_cid`);
|
(`uid` ASC, `rid` ASC, `permission`, `last_seen_cid`);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `room_item` (
|
CREATE TABLE IF NOT EXISTS `msg` (
|
||||||
`cid` INTEGER NOT NULL PRIMARY KEY,
|
`cid` INTEGER NOT NULL PRIMARY KEY,
|
||||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||||
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
`uid` INTEGER NOT NULL REFERENCES `user` ON DELETE RESTRICT,
|
||||||
|
@ -49,4 +49,4 @@ CREATE TABLE IF NOT EXISTS `room_item` (
|
||||||
`rich_text` TEXT NOT NULL
|
`rich_text` TEXT NOT NULL
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS `room_latest_item` ON `room_item` (`rid` ASC, `cid` DESC);
|
CREATE INDEX IF NOT EXISTS `room_latest_msg` ON `msg` (`rid` ASC, `cid` DESC);
|
||||||
|
|
|
@ -10,7 +10,7 @@ static INIT_SQL: &str = include_str!("../schema.sql");
|
||||||
|
|
||||||
// Simple and stupid version check for now.
|
// Simple and stupid version check for now.
|
||||||
// `echo -n 'blahd-database-0' | sha256sum | head -c5` || version
|
// `echo -n 'blahd-database-0' | sha256sum | head -c5` || version
|
||||||
const APPLICATION_ID: i32 = 0xd9e_8403;
|
const APPLICATION_ID: i32 = 0xd9e_8404;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use std::task::{Context, Poll};
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use blah_types::{AuthPayload, ChatItem, WithSig};
|
use blah_types::{AuthPayload, SignedChatMsg, WithSig};
|
||||||
use futures_util::future::Either;
|
use futures_util::future::Either;
|
||||||
use futures_util::stream::SplitSink;
|
use futures_util::stream::SplitSink;
|
||||||
use futures_util::{stream_select, SinkExt as _, Stream, StreamExt};
|
use futures_util::{stream_select, SinkExt as _, Stream, StreamExt};
|
||||||
|
@ -28,8 +28,8 @@ pub enum Incoming {}
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Outgoing<'a> {
|
pub enum Outgoing<'a> {
|
||||||
/// A chat message from a joined room.
|
/// A message from a joined room.
|
||||||
Chat(&'a ChatItem),
|
Msg(&'a SignedChatMsg),
|
||||||
/// The receiver is too slow to receive and some events and are dropped.
|
/// The receiver is too slow to receive and some events and are dropped.
|
||||||
// FIXME: Should we indefinitely buffer them or just disconnect the client instead?
|
// FIXME: Should we indefinitely buffer them or just disconnect the client instead?
|
||||||
Lagged,
|
Lagged,
|
||||||
|
@ -71,11 +71,11 @@ impl WsSenderWrapper<'_, '_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserEventSender = broadcast::Sender<Arc<ChatItem>>;
|
type UserEventSender = broadcast::Sender<Arc<SignedChatMsg>>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct UserEventReceiver {
|
struct UserEventReceiver {
|
||||||
rx: BroadcastStream<Arc<ChatItem>>,
|
rx: BroadcastStream<Arc<SignedChatMsg>>,
|
||||||
st: Arc<AppState>,
|
st: Arc<AppState>,
|
||||||
uid: u64,
|
uid: u64,
|
||||||
}
|
}
|
||||||
|
@ -93,7 +93,7 @@ impl Drop for UserEventReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stream for UserEventReceiver {
|
impl Stream for UserEventReceiver {
|
||||||
type Item = Result<Arc<ChatItem>, BroadcastStreamRecvError>;
|
type Item = Result<Arc<SignedChatMsg>, BroadcastStreamRecvError>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
self.rx.poll_next_unpin(cx)
|
self.rx.poll_next_unpin(cx)
|
||||||
|
@ -155,7 +155,7 @@ pub async fn handle_ws(st: Arc<AppState>, ws: &mut WebSocket) -> Result<Infallib
|
||||||
Either::Left(msg) => match serde_json::from_str::<Incoming>(&msg?)? {},
|
Either::Left(msg) => match serde_json::from_str::<Incoming>(&msg?)? {},
|
||||||
Either::Right(ret) => {
|
Either::Right(ret) => {
|
||||||
let msg = match &ret {
|
let msg = match &ret {
|
||||||
Ok(chat) => Outgoing::Chat(chat),
|
Ok(chat) => Outgoing::Msg(chat),
|
||||||
Err(BroadcastStreamRecvError::Lagged(_)) => Outgoing::Lagged,
|
Err(BroadcastStreamRecvError::Lagged(_)) => Outgoing::Lagged,
|
||||||
};
|
};
|
||||||
// TODO: Concurrent send.
|
// TODO: Concurrent send.
|
||||||
|
|
100
blahd/src/lib.rs
100
blahd/src/lib.rs
|
@ -11,9 +11,9 @@ 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::{
|
||||||
ChatItem, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission,
|
ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id, MemberPermission, RoomAdminOp,
|
||||||
RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, Signee, UserKey,
|
RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, SignedChatMsg, Signee, UserKey,
|
||||||
WithItemId, WithSig,
|
WithMsgId, WithSig,
|
||||||
};
|
};
|
||||||
use config::ServerConfig;
|
use config::ServerConfig;
|
||||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||||
|
@ -100,8 +100,8 @@ pub fn router(st: Arc<AppState>) -> Router {
|
||||||
.route("/room/:rid", get(room_get_metadata))
|
.route("/room/:rid", get(room_get_metadata))
|
||||||
// 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/item", get(room_item_list).post(room_item_post))
|
.route("/room/:rid/msg", get(room_msg_list).post(room_msg_post))
|
||||||
.route("/room/:rid/item/:cid/seen", post(room_item_mark_seen))
|
.route("/room/:rid/msg/:cid/seen", post(room_msg_mark_seen))
|
||||||
.route("/room/:rid/admin", post(room_admin))
|
.route("/room/:rid/admin", post(room_admin))
|
||||||
.layer(tower_http::limit::RequestBodyLimitLayer::new(
|
.layer(tower_http::limit::RequestBodyLimitLayer::new(
|
||||||
st.config.max_request_len,
|
st.config.max_request_len,
|
||||||
|
@ -188,12 +188,12 @@ async fn room_list(
|
||||||
.query_map(params, |row| {
|
.query_map(params, |row| {
|
||||||
// TODO: Extract this into a function.
|
// TODO: Extract this into a function.
|
||||||
let rid = row.get("rid")?;
|
let rid = row.get("rid")?;
|
||||||
let last_item = row
|
let last_msg = row
|
||||||
.get::<_, Option<Id>>("cid")?
|
.get::<_, Option<Id>>("cid")?
|
||||||
.map(|cid| {
|
.map(|cid| {
|
||||||
Ok::<_, rusqlite::Error>(WithItemId {
|
Ok::<_, rusqlite::Error>(WithMsgId {
|
||||||
cid,
|
cid,
|
||||||
item: ChatItem {
|
msg: SignedChatMsg {
|
||||||
sig: row.get("sig")?,
|
sig: row.get("sig")?,
|
||||||
signee: Signee {
|
signee: Signee {
|
||||||
nonce: row.get("nonce")?,
|
nonce: row.get("nonce")?,
|
||||||
|
@ -212,7 +212,7 @@ async fn room_list(
|
||||||
rid,
|
rid,
|
||||||
title: row.get("title")?,
|
title: row.get("title")?,
|
||||||
attrs: row.get("attrs")?,
|
attrs: row.get("attrs")?,
|
||||||
last_item,
|
last_msg,
|
||||||
last_seen_cid: Some(row.get::<_, Id>("last_seen_cid")?)
|
last_seen_cid: Some(row.get::<_, Id>("last_seen_cid")?)
|
||||||
.filter(|cid| cid.0 != 0),
|
.filter(|cid| cid.0 != 0),
|
||||||
unseen_cnt: row.get("unseen_cnt").ok(),
|
unseen_cnt: row.get("unseen_cnt").ok(),
|
||||||
|
@ -232,7 +232,7 @@ async fn room_list(
|
||||||
SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`,
|
SELECT `rid`, `title`, `attrs`, 0 AS `last_seen_cid`,
|
||||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
|
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`
|
||||||
FROM `room`
|
FROM `room`
|
||||||
LEFT JOIN `room_item` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` USING (`uid`)
|
LEFT JOIN `user` AS `last_author` USING (`uid`)
|
||||||
WHERE `rid` > :start_rid AND
|
WHERE `rid` > :start_rid AND
|
||||||
(`attrs` & :perm) = :perm
|
(`attrs` & :perm) = :perm
|
||||||
|
@ -257,8 +257,8 @@ async fn room_list(
|
||||||
FROM `user`
|
FROM `user`
|
||||||
JOIN `room_member` USING (`uid`)
|
JOIN `room_member` USING (`uid`)
|
||||||
JOIN `room` USING (`rid`)
|
JOIN `room` USING (`rid`)
|
||||||
LEFT JOIN `room_item` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `room_item`.`uid`)
|
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
||||||
LEFT JOIN `user` AS `peer_user` ON
|
LEFT JOIN `user` AS `peer_user` ON
|
||||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
||||||
WHERE `user`.`userkey` = :userkey AND
|
WHERE `user`.`userkey` = :userkey AND
|
||||||
|
@ -283,14 +283,14 @@ async fn room_list(
|
||||||
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
`cid`, `last_author`.`userkey`, `timestamp`, `nonce`, `sig`, `rich_text`,
|
||||||
`peer_user`.`userkey` AS `peer_userkey`,
|
`peer_user`.`userkey` AS `peer_userkey`,
|
||||||
(SELECT COUNT(*)
|
(SELECT COUNT(*)
|
||||||
FROM `room_item` AS `unseen_item`
|
FROM `msg` AS `unseen_msg`
|
||||||
WHERE `unseen_item`.`rid` = `room`.`rid` AND
|
WHERE `unseen_msg`.`rid` = `room`.`rid` AND
|
||||||
`last_seen_cid` < `unseen_item`.`cid`) AS `unseen_cnt`
|
`last_seen_cid` < `unseen_msg`.`cid`) AS `unseen_cnt`
|
||||||
FROM `user`
|
FROM `user`
|
||||||
JOIN `room_member` USING (`uid`)
|
JOIN `room_member` USING (`uid`)
|
||||||
JOIN `room` USING (`rid`)
|
JOIN `room` USING (`rid`)
|
||||||
LEFT JOIN `room_item` USING (`rid`)
|
LEFT JOIN `msg` USING (`rid`)
|
||||||
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `room_item`.`uid`)
|
LEFT JOIN `user` AS `last_author` ON (`last_author`.`uid` = `msg`.`uid`)
|
||||||
LEFT JOIN `user` AS `peer_user` ON
|
LEFT JOIN `user` AS `peer_user` ON
|
||||||
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
(`peer_user`.`uid` = `room`.`peer1` + `room`.`peer2` - `user`.`uid`)
|
||||||
WHERE `user`.`userkey` = :userkey AND
|
WHERE `user`.`userkey` = :userkey AND
|
||||||
|
@ -536,7 +536,7 @@ struct Pagination {
|
||||||
/// Maximum page size.
|
/// Maximum page size.
|
||||||
top: Option<NonZeroUsize>,
|
top: Option<NonZeroUsize>,
|
||||||
/// Only return items before (excluding) this token.
|
/// Only return items before (excluding) this token.
|
||||||
/// Useful for `room_item_list` to pass `last_seen_cid` without over-fetching.
|
/// Useful for `room_msg_list` to pass `last_seen_cid` without over-fetching.
|
||||||
until_token: Option<Id>,
|
until_token: Option<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -550,24 +550,24 @@ impl Pagination {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct RoomItems {
|
pub struct RoomMsgs {
|
||||||
pub items: Vec<WithItemId<ChatItem>>,
|
pub msgs: Vec<WithMsgId<SignedChatMsg>>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub skip_token: Option<Id>,
|
pub skip_token: Option<Id>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_item_list(
|
async fn room_msg_list(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
R(Path(rid), _): RE<Path<Id>>,
|
R(Path(rid), _): RE<Path<Id>>,
|
||||||
R(Query(pagination), _): RE<Query<Pagination>>,
|
R(Query(pagination), _): RE<Query<Pagination>>,
|
||||||
auth: MaybeAuth,
|
auth: MaybeAuth,
|
||||||
) -> Result<Json<RoomItems>, ApiError> {
|
) -> Result<Json<RoomMsgs>, ApiError> {
|
||||||
let (items, skip_token) = {
|
let (msgs, skip_token) = {
|
||||||
let conn = st.db.get();
|
let conn = st.db.get();
|
||||||
get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |_row| Ok(()))?;
|
get_room_if_readable(&conn, rid, auth.into_optional()?.as_ref(), |_row| Ok(()))?;
|
||||||
query_room_items(&st, &conn, rid, pagination)?
|
query_room_msgs(&st, &conn, rid, pagination)?
|
||||||
};
|
};
|
||||||
Ok(Json(RoomItems { items, skip_token }))
|
Ok(Json(RoomMsgs { msgs, skip_token }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_get_metadata(
|
async fn room_get_metadata(
|
||||||
|
@ -589,7 +589,7 @@ async fn room_get_metadata(
|
||||||
attrs,
|
attrs,
|
||||||
|
|
||||||
// TODO: Should we include these here?
|
// TODO: Should we include these here?
|
||||||
last_item: None,
|
last_msg: None,
|
||||||
last_seen_cid: None,
|
last_seen_cid: None,
|
||||||
unseen_cnt: None,
|
unseen_cnt: None,
|
||||||
member_permission: None,
|
member_permission: None,
|
||||||
|
@ -603,28 +603,28 @@ async fn room_get_feed(
|
||||||
R(Query(pagination), _): RE<Query<Pagination>>,
|
R(Query(pagination), _): RE<Query<Pagination>>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let title;
|
let title;
|
||||||
let (items, skip_token) = {
|
let (msgs, skip_token) = {
|
||||||
let conn = st.db.get();
|
let conn = st.db.get();
|
||||||
title = get_room_if_readable(&conn, rid, None, |row| row.get::<_, String>("title"))?;
|
title = get_room_if_readable(&conn, rid, None, |row| row.get::<_, String>("title"))?;
|
||||||
query_room_items(&st, &conn, rid, pagination)?
|
query_room_msgs(&st, &conn, rid, pagination)?
|
||||||
};
|
};
|
||||||
|
|
||||||
let items = items
|
let items = msgs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|WithItemId { cid, item }| {
|
.map(|WithMsgId { cid, msg }| {
|
||||||
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(item.signee.timestamp);
|
let time = SystemTime::UNIX_EPOCH + Duration::from_secs(msg.signee.timestamp);
|
||||||
let author = FeedAuthor {
|
let author = FeedAuthor {
|
||||||
name: item.signee.user.to_string(),
|
name: msg.signee.user.to_string(),
|
||||||
};
|
};
|
||||||
FeedItem {
|
FeedItem {
|
||||||
id: cid.to_string(),
|
id: cid.to_string(),
|
||||||
content_html: item.signee.payload.rich_text.html().to_string(),
|
content_html: msg.signee.payload.rich_text.html().to_string(),
|
||||||
date_published: humantime::format_rfc3339(time).to_string(),
|
date_published: humantime::format_rfc3339(time).to_string(),
|
||||||
authors: (author,),
|
authors: (author,),
|
||||||
extra: FeedItemExtra {
|
extra: FeedItemExtra {
|
||||||
timestamp: item.signee.timestamp,
|
timestamp: msg.signee.timestamp,
|
||||||
nonce: item.signee.nonce,
|
nonce: msg.signee.nonce,
|
||||||
sig: item.sig,
|
sig: msg.sig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -734,19 +734,19 @@ fn get_room_if_readable<T>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get room items with pagination parameters,
|
/// Get room messages with pagination parameters,
|
||||||
/// return a page of items and the next skip_token if this is not the last page.
|
/// return a page of messages and the next `skip_token` if this is not the last page.
|
||||||
fn query_room_items(
|
fn query_room_msgs(
|
||||||
st: &AppState,
|
st: &AppState,
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
rid: Id,
|
rid: Id,
|
||||||
pagination: Pagination,
|
pagination: Pagination,
|
||||||
) -> Result<(Vec<WithItemId<ChatItem>>, Option<Id>), ApiError> {
|
) -> Result<(Vec<WithMsgId<SignedChatMsg>>, Option<Id>), ApiError> {
|
||||||
let page_len = pagination.effective_page_len(st);
|
let page_len = pagination.effective_page_len(st);
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
r"
|
r"
|
||||||
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
||||||
FROM `room_item`
|
FROM `msg`
|
||||||
JOIN `user` USING (`uid`)
|
JOIN `user` USING (`uid`)
|
||||||
WHERE `rid` = :rid AND
|
WHERE `rid` = :rid AND
|
||||||
:after_cid < `cid` AND
|
:after_cid < `cid` AND
|
||||||
|
@ -755,7 +755,7 @@ fn query_room_items(
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
let items = stmt
|
let msgs = stmt
|
||||||
.query_and_then(
|
.query_and_then(
|
||||||
named_params! {
|
named_params! {
|
||||||
":rid": rid,
|
":rid": rid,
|
||||||
|
@ -764,9 +764,9 @@ fn query_room_items(
|
||||||
":limit": page_len,
|
":limit": page_len,
|
||||||
},
|
},
|
||||||
|row| {
|
|row| {
|
||||||
Ok(WithItemId {
|
Ok(WithMsgId {
|
||||||
cid: row.get("cid")?,
|
cid: row.get("cid")?,
|
||||||
item: ChatItem {
|
msg: SignedChatMsg {
|
||||||
sig: row.get("sig")?,
|
sig: row.get("sig")?,
|
||||||
signee: Signee {
|
signee: Signee {
|
||||||
nonce: row.get("nonce")?,
|
nonce: row.get("nonce")?,
|
||||||
|
@ -783,12 +783,12 @@ fn query_room_items(
|
||||||
)?
|
)?
|
||||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||||
let skip_token =
|
let skip_token =
|
||||||
(items.len() == page_len).then(|| items.last().expect("page must not be empty").cid);
|
(msgs.len() == page_len).then(|| msgs.last().expect("page must not be empty").cid);
|
||||||
|
|
||||||
Ok((items, skip_token))
|
Ok((msgs, skip_token))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_item_post(
|
async fn room_msg_post(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
R(Path(rid), _): RE<Path<Id>>,
|
R(Path(rid), _): RE<Path<Id>>,
|
||||||
SignedJson(chat): SignedJson<ChatPayload>,
|
SignedJson(chat): SignedJson<ChatPayload>,
|
||||||
|
@ -836,14 +836,14 @@ async fn room_item_post(
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::FORBIDDEN,
|
StatusCode::FORBIDDEN,
|
||||||
"permission_denied",
|
"permission_denied",
|
||||||
"the user does not have permission to post item in the room",
|
"the user does not have permission to post in the room",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cid = Id::gen();
|
let cid = Id::gen();
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r"
|
r"
|
||||||
INSERT INTO `room_item` (`cid`, `rid`, `uid`, `timestamp`, `nonce`, `sig`, `rich_text`)
|
INSERT INTO `msg` (`cid`, `rid`, `uid`, `timestamp`, `nonce`, `sig`, `rich_text`)
|
||||||
VALUES (:cid, :rid, :uid, :timestamp, :nonce, :sig, :rich_text)
|
VALUES (:cid, :rid, :uid, :timestamp, :nonce, :sig, :rich_text)
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {
|
||||||
|
@ -1048,7 +1048,7 @@ async fn room_leave(st: &AppState, rid: Id, user: UserKey) -> Result<(), ApiErro
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn room_item_mark_seen(
|
async fn room_msg_mark_seen(
|
||||||
st: ArcState,
|
st: ArcState,
|
||||||
R(Path((rid, cid)), _): RE<Path<(Id, u64)>>,
|
R(Path((rid, cid)), _): RE<Path<(Id, u64)>>,
|
||||||
Auth(user): Auth,
|
Auth(user): Auth,
|
||||||
|
|
|
@ -7,11 +7,11 @@ use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
get_timestamp, AuthPayload, ChatItem, ChatPayload, CreateGroup, CreatePeerChat,
|
get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id,
|
||||||
CreateRoomPayload, Id, MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs,
|
MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMember,
|
||||||
RoomMember, RoomMemberList, RoomMetadata, ServerPermission, UserKey, WithItemId, WithSig,
|
RoomMemberList, RoomMetadata, ServerPermission, SignedChatMsg, UserKey, WithMsgId, WithSig,
|
||||||
};
|
};
|
||||||
use blahd::{ApiError, AppState, Database, RoomItems, RoomList};
|
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
use futures_util::TryFutureExt;
|
use futures_util::TryFutureExt;
|
||||||
use rand::rngs::mock::StepRng;
|
use rand::rngs::mock::StepRng;
|
||||||
|
@ -176,8 +176,8 @@ impl Server {
|
||||||
rid: Id,
|
rid: Id,
|
||||||
key: &SigningKey,
|
key: &SigningKey,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> impl Future<Output = Result<WithItemId<ChatItem>>> + use<'_> {
|
) -> impl Future<Output = Result<WithMsgId<SignedChatMsg>>> + use<'_> {
|
||||||
let item = sign(
|
let msg = sign(
|
||||||
key,
|
key,
|
||||||
&mut *self.rng.borrow_mut(),
|
&mut *self.rng.borrow_mut(),
|
||||||
ChatPayload {
|
ChatPayload {
|
||||||
|
@ -189,13 +189,13 @@ impl Server {
|
||||||
let cid = self
|
let cid = self
|
||||||
.request::<_, Id>(
|
.request::<_, Id>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("/room/{rid}/item"),
|
&format!("/room/{rid}/msg"),
|
||||||
None,
|
None,
|
||||||
Some(item.clone()),
|
Some(msg.clone()),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Ok(WithItemId { cid, item })
|
Ok(WithMsgId { cid, msg })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,7 +263,7 @@ async fn room_create_get(server: Server, ref mut rng: impl RngCore, #[case] publ
|
||||||
} else {
|
} else {
|
||||||
RoomAttrs::empty()
|
RoomAttrs::empty()
|
||||||
},
|
},
|
||||||
last_item: None,
|
last_msg: None,
|
||||||
last_seen_cid: None,
|
last_seen_cid: None,
|
||||||
unseen_cnt: None,
|
unseen_cnt: None,
|
||||||
member_permission: None,
|
member_permission: None,
|
||||||
|
@ -406,7 +406,7 @@ async fn room_join_leave(server: Server, ref mut rng: impl RngCore) {
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
|
async fn room_chat_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
let rid_pub = server
|
let rid_pub = server
|
||||||
.create_room(
|
.create_room(
|
||||||
&ALICE_PRIV,
|
&ALICE_PRIV,
|
||||||
|
@ -430,9 +430,9 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let post = |rid: Id, chat: ChatItem| {
|
let post = |rid: Id, chat: SignedChatMsg| {
|
||||||
server
|
server
|
||||||
.request::<_, Id>(Method::POST, &format!("/room/{rid}/item"), None, Some(chat))
|
.request::<_, Id>(Method::POST, &format!("/room/{rid}/msg"), None, Some(chat))
|
||||||
.map_ok(|opt| opt.unwrap())
|
.map_ok(|opt| opt.unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -471,94 +471,88 @@ async fn room_item_post_read(server: Server, ref mut rng: impl RngCore) {
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
//// Item listing ////
|
//// Msgs listing ////
|
||||||
|
|
||||||
let chat1 = WithItemId::new(cid1, chat1);
|
let chat1 = WithMsgId::new(cid1, chat1);
|
||||||
let chat2 = WithItemId::new(cid2, chat2);
|
let chat2 = WithMsgId::new(cid2, chat2);
|
||||||
|
|
||||||
// List with default page size.
|
// List with default page size.
|
||||||
let items = server
|
let msgs = server
|
||||||
.get::<RoomItems>(&format!("/room/{rid_pub}/item"), None)
|
.get::<RoomMsgs>(&format!("/room/{rid_pub}/msg"), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
items,
|
msgs,
|
||||||
RoomItems {
|
RoomMsgs {
|
||||||
items: vec![chat2.clone(), chat1.clone()],
|
msgs: vec![chat2.clone(), chat1.clone()],
|
||||||
skip_token: None,
|
skip_token: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// List with small page size.
|
// List with small page size.
|
||||||
let items = server
|
let msgs = server
|
||||||
.get::<RoomItems>(&format!("/room/{rid_pub}/item?top=1"), None)
|
.get::<RoomMsgs>(&format!("/room/{rid_pub}/msg?top=1"), None)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
items,
|
msgs,
|
||||||
RoomItems {
|
RoomMsgs {
|
||||||
items: vec![chat2.clone()],
|
msgs: vec![chat2.clone()],
|
||||||
skip_token: Some(cid2),
|
skip_token: Some(cid2),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second page.
|
// Second page.
|
||||||
let items = server
|
let msgs = server
|
||||||
.get::<RoomItems>(
|
.get::<RoomMsgs>(&format!("/room/{rid_pub}/msg?skipToken={cid2}&top=1"), None)
|
||||||
&format!("/room/{rid_pub}/item?skipToken={cid2}&top=1"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
items,
|
msgs,
|
||||||
RoomItems {
|
RoomMsgs {
|
||||||
items: vec![chat1.clone()],
|
msgs: vec![chat1.clone()],
|
||||||
skip_token: Some(cid1),
|
skip_token: Some(cid1),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// No more.
|
// No more.
|
||||||
let items = server
|
let msgs = server
|
||||||
.get::<RoomItems>(
|
.get::<RoomMsgs>(&format!("/room/{rid_pub}/msg?skipToken={cid1}&top=1"), None)
|
||||||
&format!("/room/{rid_pub}/item?skipToken={cid1}&top=1"),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(items, RoomItems::default());
|
assert_eq!(msgs, RoomMsgs::default());
|
||||||
|
|
||||||
//// Private room ////
|
//// Private room ////
|
||||||
|
|
||||||
// Access without token.
|
// Access without token.
|
||||||
server
|
server
|
||||||
.get::<RoomItems>(&format!("/room/{rid_priv}/item"), None)
|
.get::<RoomMsgs>(&format!("/room/{rid_priv}/msg"), None)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Not a member.
|
// Not a member.
|
||||||
server
|
server
|
||||||
.get::<RoomItems>(
|
.get::<RoomMsgs>(
|
||||||
&format!("/room/{rid_priv}/item"),
|
&format!("/room/{rid_priv}/msg"),
|
||||||
Some(&auth(&BOB_PRIV, rng)),
|
Some(&auth(&BOB_PRIV, rng)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
.expect_api_err(StatusCode::NOT_FOUND, "not_found");
|
||||||
|
|
||||||
// Ok.
|
// Ok.
|
||||||
let items = server
|
let msgs = server
|
||||||
.get::<RoomItems>(
|
.get::<RoomMsgs>(
|
||||||
&format!("/room/{rid_priv}/item"),
|
&format!("/room/{rid_priv}/msg"),
|
||||||
Some(&auth(&ALICE_PRIV, rng)),
|
Some(&auth(&ALICE_PRIV, rng)),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(items, RoomItems::default());
|
assert_eq!(msgs, RoomMsgs::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
async fn last_seen(server: Server, ref mut rng: impl RngCore) {
|
||||||
let title = "public room";
|
let title = "public room";
|
||||||
let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE;
|
let attrs = RoomAttrs::PUBLIC_READABLE | RoomAttrs::PUBLIC_JOINABLE;
|
||||||
let member_perm = MemberPermission::ALL;
|
let member_perm = MemberPermission::ALL;
|
||||||
|
@ -571,7 +565,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
let alice_chat1 = server.post_chat(rid, &ALICE_PRIV, "alice1").await.unwrap();
|
let alice_chat1 = server.post_chat(rid, &ALICE_PRIV, "alice1").await.unwrap();
|
||||||
let alice_chat2 = server.post_chat(rid, &ALICE_PRIV, "alice2").await.unwrap();
|
let alice_chat2 = server.post_chat(rid, &ALICE_PRIV, "alice2").await.unwrap();
|
||||||
|
|
||||||
// 2 new items.
|
// 2 new msgs.
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
||||||
.await
|
.await
|
||||||
|
@ -583,7 +577,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
rid,
|
rid,
|
||||||
title: Some(title.into()),
|
title: Some(title.into()),
|
||||||
attrs,
|
attrs,
|
||||||
last_item: Some(alice_chat2.clone()),
|
last_msg: Some(alice_chat2.clone()),
|
||||||
last_seen_cid: None,
|
last_seen_cid: None,
|
||||||
unseen_cnt: Some(2),
|
unseen_cnt: Some(2),
|
||||||
member_permission: Some(member_perm),
|
member_permission: Some(member_perm),
|
||||||
|
@ -596,7 +590,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
let seen = |key: &SigningKey, cid: Id| {
|
let seen = |key: &SigningKey, cid: Id| {
|
||||||
server.request::<NoContent, NoContent>(
|
server.request::<NoContent, NoContent>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
&format!("/room/{rid}/item/{cid}/seen"),
|
&format!("/room/{rid}/msg/{cid}/seen"),
|
||||||
Some(&auth(key, &mut *server.rng.borrow_mut())),
|
Some(&auth(key, &mut *server.rng.borrow_mut())),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
@ -605,7 +599,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
// Mark the first one seen.
|
// Mark the first one seen.
|
||||||
seen(&ALICE_PRIV, alice_chat1.cid).await.unwrap();
|
seen(&ALICE_PRIV, alice_chat1.cid).await.unwrap();
|
||||||
|
|
||||||
// 1 new item.
|
// 1 new msg.
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
||||||
.await
|
.await
|
||||||
|
@ -617,7 +611,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
rid,
|
rid,
|
||||||
title: Some(title.into()),
|
title: Some(title.into()),
|
||||||
attrs,
|
attrs,
|
||||||
last_item: Some(alice_chat2.clone()),
|
last_msg: Some(alice_chat2.clone()),
|
||||||
last_seen_cid: Some(alice_chat1.cid),
|
last_seen_cid: Some(alice_chat1.cid),
|
||||||
unseen_cnt: Some(1),
|
unseen_cnt: Some(1),
|
||||||
member_permission: Some(member_perm),
|
member_permission: Some(member_perm),
|
||||||
|
@ -627,7 +621,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark the second one seen. Now there is no new items.
|
// Mark the second one seen. Now there is no new messages.
|
||||||
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
||||||
|
@ -635,7 +629,7 @@ async fn last_seen_item(server: Server, ref mut rng: impl RngCore) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(rooms, RoomList::default());
|
assert_eq!(rooms, RoomList::default());
|
||||||
|
|
||||||
// Marking a seen item seen is a no-op.
|
// Marking a seen message seen is a no-op.
|
||||||
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
seen(&ALICE_PRIV, alice_chat2.cid).await.unwrap();
|
||||||
let rooms = server
|
let rooms = server
|
||||||
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
.get::<RoomList>("/room?filter=unseen", Some(&auth(&ALICE_PRIV, rng)))
|
||||||
|
@ -688,7 +682,7 @@ async fn peer_chat(server: Server, ref mut rng: impl RngCore) {
|
||||||
rid,
|
rid,
|
||||||
title: None,
|
title: None,
|
||||||
attrs: RoomAttrs::PEER_CHAT,
|
attrs: RoomAttrs::PEER_CHAT,
|
||||||
last_item: None,
|
last_msg: None,
|
||||||
last_seen_cid: None,
|
last_seen_cid: None,
|
||||||
unseen_cnt: None,
|
unseen_cnt: None,
|
||||||
member_permission: None,
|
member_permission: None,
|
||||||
|
|
|
@ -72,8 +72,8 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
The maximum number of items returned in each page. This is only an
|
The maximum count of rooms returned in a single response. This is
|
||||||
advice and server can clamp it to a smaller value.
|
only an advice and server can clamp it to a smaller value.
|
||||||
|
|
||||||
- name: skipToken
|
- name: skipToken
|
||||||
in: query
|
in: query
|
||||||
|
@ -107,7 +107,7 @@ paths:
|
||||||
|
|
||||||
/room/create:
|
/room/create:
|
||||||
post:
|
post:
|
||||||
summary: Create room
|
summary: Create a room
|
||||||
|
|
||||||
description:
|
description:
|
||||||
When `typ="create_room"`, create a multi-user room.
|
When `typ="create_room"`, create a multi-user room.
|
||||||
|
@ -229,13 +229,13 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiError'
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
/room/{rid}/item:
|
/room/{rid}/msg:
|
||||||
get:
|
get:
|
||||||
summary: List items in room
|
summary: List messages in a room
|
||||||
description: |
|
description: |
|
||||||
Return items in reversed time order, up to `skipToken` items in a
|
Return a list of messages in reversed server time order, up to length `top`
|
||||||
single response, from room {rid}.
|
in a single response, from room {rid}.
|
||||||
The last (oldest) chat `cid` will be returned as `skipToken` in
|
The last (oldest) message's `cid` will be returned as `skipToken` in
|
||||||
response, which can be used as query parameter for the next GET, to
|
response, which can be used as query parameter for the next GET, to
|
||||||
repeatedly fetch more history.
|
repeatedly fetch more history.
|
||||||
|
|
||||||
|
@ -251,8 +251,9 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: integer
|
type: integer
|
||||||
description: |
|
description: |
|
||||||
The maximum number of items to return. This is an advice and may be
|
The number of items returned in a single response. This is
|
||||||
further clamped by the server. It must not be zero.
|
an advice and may be further clamped by the server. It must not be
|
||||||
|
zero.
|
||||||
|
|
||||||
- name: skipToken
|
- name: skipToken
|
||||||
in: query
|
in: query
|
||||||
|
@ -267,7 +268,7 @@ paths:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/RoomItems'
|
$ref: '#/components/schemas/RoomMsgs'
|
||||||
|
|
||||||
404:
|
404:
|
||||||
description: |
|
description: |
|
||||||
|
@ -278,7 +279,7 @@ paths:
|
||||||
$ref: '#/components/schemas/ApiError'
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Post item in room
|
summary: Post a `Msg` into a room
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
|
@ -291,7 +292,7 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: Newly created item `cid`.
|
description: Newly created message id `cid`.
|
||||||
|
|
||||||
403:
|
403:
|
||||||
description: The user does not have permission to post in this room.
|
description: The user does not have permission to post in this room.
|
||||||
|
@ -307,15 +308,16 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ApiError'
|
$ref: '#/components/schemas/ApiError'
|
||||||
|
|
||||||
/room/{rid}/item/{cid}/seen:
|
/room/{rid}/msg/{cid}/seen:
|
||||||
post:
|
post:
|
||||||
summary: Mark item seen
|
summary: Mark a message seen
|
||||||
description: |
|
description: |
|
||||||
Mark item {cid} in room {rid} seen by the current user.
|
Mark message {cid} and everything before it in room {rid} seen by the
|
||||||
|
current user.
|
||||||
|
|
||||||
Server may enforce that last seen item does not go backward. Marking
|
Server may enforce that last seen message does not go backward. Marking
|
||||||
an older item seen or sending the same request multiple times can be a
|
an older message seen or sending the same request multiple times can be
|
||||||
no-op.
|
a no-op.
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
- name: Authorization
|
- name: Authorization
|
||||||
|
@ -398,8 +400,8 @@ components:
|
||||||
description: Room attributes bitset, see `RoomAttrs`.
|
description: Room attributes bitset, see `RoomAttrs`.
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
last_item:
|
last_msg:
|
||||||
$ref: '#/components/schemas/WithItemId-WithSig-Chat'
|
$ref: '#/components/schemas/WithMsgId-WithSig-Chat'
|
||||||
last_seen_cid:
|
last_seen_cid:
|
||||||
description: The `cid` of the last chat being marked as seen.
|
description: The `cid` of the last chat being marked as seen.
|
||||||
type: string
|
type: string
|
||||||
|
@ -429,16 +431,16 @@ components:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
|
|
||||||
RoomItems:
|
RoomMsgs:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- items
|
- msgs
|
||||||
properties:
|
properties:
|
||||||
items:
|
msgs:
|
||||||
description: Room items in reversed server-received time order.
|
description: Room messages in reversed server-received time order.
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/WithItemId-WithSig-Chat'
|
$ref: '#/components/schemas/WithMsgId-WithSig-Chat'
|
||||||
skip_token:
|
skip_token:
|
||||||
description: The token for fetching the next page.
|
description: The token for fetching the next page.
|
||||||
type: string
|
type: string
|
||||||
|
@ -581,14 +583,14 @@ components:
|
||||||
timestamp: 1724966284
|
timestamp: 1724966284
|
||||||
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
|
||||||
|
|
||||||
WithItemId-WithSig-Chat:
|
WithMsgId-WithSig-Chat:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/WithSig-Chat'
|
- $ref: '#/components/schemas/WithSig-Chat'
|
||||||
- type: object
|
- type: object
|
||||||
properties:
|
properties:
|
||||||
cid:
|
cid:
|
||||||
type: string
|
type: string
|
||||||
description: An opaque server-specific item identifier.
|
description: An opaque server-specific identifier.
|
||||||
|
|
||||||
WithSig-CreateRoom:
|
WithSig-CreateRoom:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
@ -186,15 +186,15 @@ async function enterRoom(rid) {
|
||||||
});
|
});
|
||||||
|
|
||||||
genAuthHeader()
|
genAuthHeader()
|
||||||
.then(opts => fetch(`${serverUrl}/room/${rid}/item`, opts))
|
.then(opts => fetch(`${serverUrl}/room/${rid}/msg`, opts))
|
||||||
.then(async (resp) => { return [resp.status, await resp.json()]; })
|
.then(async (resp) => { return [resp.status, await resp.json()]; })
|
||||||
.then(async ([status, json]) => {
|
.then(async ([status, json]) => {
|
||||||
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`);
|
if (status !== 200) throw new Error(`status ${status}: ${json.error.message}`);
|
||||||
const { items } = json
|
const { msgs } = json
|
||||||
items.reverse();
|
msgs.reverse();
|
||||||
for (const chat of items) {
|
for (const msg of msgs) {
|
||||||
lastCid = chat.cid;
|
lastCid = msg.cid;
|
||||||
await showChatMsg(chat);
|
await showChatMsg(msg);
|
||||||
}
|
}
|
||||||
log('---history---');
|
log('---history---');
|
||||||
})
|
})
|
||||||
|
@ -242,12 +242,12 @@ async function connectServer(newServerUrl) {
|
||||||
if (msg.chat.signee.payload.room === curRoom) {
|
if (msg.chat.signee.payload.room === curRoom) {
|
||||||
await showChatMsg(msg.chat);
|
await showChatMsg(msg.chat);
|
||||||
} else {
|
} else {
|
||||||
console.log('ignore background room item');
|
console.log('ignore background room msg');
|
||||||
}
|
}
|
||||||
} else if (msg.lagged !== undefined) {
|
} else if (msg.lagged !== undefined) {
|
||||||
log('some events are dropped because of queue overflow')
|
log('some events are dropped because of queue overflow')
|
||||||
} else {
|
} else {
|
||||||
log(`unknown ws message: ${e.data}`);
|
log(`unknown ws msg: ${e.data}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -269,11 +269,11 @@ async function loadRoomList(autoJoin) {
|
||||||
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}`);
|
||||||
for (const { rid, title, attrs, last_item, last_seen_cid } of json.rooms) {
|
for (const { rid, title, attrs, last_msg, last_seen_cid } of json.rooms) {
|
||||||
const el = document.createElement('option');
|
const el = document.createElement('option');
|
||||||
el.value = rid;
|
el.value = rid;
|
||||||
el.innerText = `${title} (rid=${rid}, attrs=${attrs})`;
|
el.innerText = `${title} (rid=${rid}, attrs=${attrs})`;
|
||||||
if (last_item !== undefined && last_item.cid !== last_seen_cid) {
|
if (last_msg !== undefined && last_msg.cid !== last_seen_cid) {
|
||||||
el.innerText += ' (unread)';
|
el.innerText += ' (unread)';
|
||||||
}
|
}
|
||||||
targetEl.appendChild(el);
|
targetEl.appendChild(el);
|
||||||
|
@ -381,7 +381,7 @@ async function postChat(text) {
|
||||||
} else {
|
} else {
|
||||||
richText = [text];
|
richText = [text];
|
||||||
}
|
}
|
||||||
await signAndPost(`${serverUrl}/room/${curRoom}/item`, {
|
await signAndPost(`${serverUrl}/room/${curRoom}/msg`, {
|
||||||
// sorted fields.
|
// sorted fields.
|
||||||
rich_text: richText,
|
rich_text: richText,
|
||||||
room: curRoom,
|
room: curRoom,
|
||||||
|
@ -398,7 +398,7 @@ async function postChat(text) {
|
||||||
|
|
||||||
async function markSeen() {
|
async function markSeen() {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(`${serverUrl}/room/${curRoom}/item/${lastCid}/seen`, {
|
const resp = await fetch(`${serverUrl}/room/${curRoom}/msg/${lastCid}/seen`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: (await genAuthHeader()).headers,
|
headers: (await genAuthHeader()).headers,
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue