feat(webapi): impl room member listing

This commit is contained in:
oxalica 2024-10-01 06:31:53 -04:00
parent bc6e6c2056
commit 367f6d2a4b
7 changed files with 250 additions and 18 deletions

View file

@ -242,19 +242,26 @@ pub trait TransactionOps {
.ok_or(ApiError::RoomNotFound)
}
// FIXME: Eliminate this.
// Currently broadcasting msgs requires traversing over all members.
fn list_room_members(&self, rid: Id) -> Result<Vec<i64>> {
fn list_room_members(
&self,
rid: Id,
start_uid: Id,
page_len: Option<NonZero<u32>>,
) -> Result<Vec<(i64, PubKey, MemberPermission, Id)>> {
let page_len = page_len.map_or(-1i64, |v| v.get().into());
prepare_cached_and_bind!(
self.conn(),
r"
SELECT `uid`
SELECT `uid`, `id_key`, `room_member`.`permission`, `last_seen_cid`
FROM `room_member`
WHERE `rid` = :rid
JOIN `user` USING (`uid`)
WHERE `rid` = :rid AND
`uid` > :start_uid
LIMIT :page_len
"
)
.raw_query()
.mapped(|row| row.get::<_, i64>(0))
.mapped(|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)))
.collect::<rusqlite::Result<Vec<_>>>()
.map_err(Into::into)
}

View file

@ -17,7 +17,7 @@ use blah_types::msg::{
SignedChatMsgWithId, UserRegisterPayload,
};
use blah_types::server::{RoomMetadata, ServerCapabilities, ServerMetadata, UserRegisterChallenge};
use blah_types::{get_timestamp, Id, Signed, UserKey};
use blah_types::{get_timestamp, Id, PubKey, Signed, UserKey};
use data_encoding::BASE64_NOPAD;
use database::{Transaction, TransactionOps};
use feed::FeedData;
@ -159,6 +159,7 @@ pub fn router(st: Arc<AppState>) -> Router {
.route("/room/:rid/msg", get(room_msg_list).post(room_msg_post))
.route("/room/:rid/msg/:cid/seen", post(room_msg_mark_seen))
.route("/room/:rid/admin", post(room_admin))
.route("/room/:rid/member", get(room_member_list))
.layer(tower_http::limit::RequestBodyLimitLayer::new(
st.config.max_request_len,
))
@ -554,7 +555,11 @@ async fn room_msg_post(
let cid = Id::gen();
txn.add_room_chat_msg(rid, uid, cid, &chat)?;
let members = txn.list_room_members(rid)?;
let members = txn
.list_room_members(rid, Id::MIN, None)?
.into_iter()
.map(|(uid, ..)| uid)
.collect::<Vec<_>>();
Ok((cid, members))
})?;
@ -668,3 +673,64 @@ async fn room_msg_mark_seen(
})?;
Ok(StatusCode::NO_CONTENT)
}
// TODO: Hoist these into types crate.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomMemberList {
pub members: Vec<RoomMember>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_token: Option<Id>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoomMember {
pub id_key: PubKey,
pub permission: MemberPermission,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_seen_cid: Option<Id>,
}
async fn room_member_list(
st: ArcState,
R(Path(rid), _): RE<Path<Id>>,
R(Query(pagination), _): RE<Query<Pagination>>,
Auth(user): Auth,
) -> Result<Json<RoomMemberList>, ApiError> {
api_ensure!(
pagination.until_token.is_none(),
"untilToken is not supported for this API"
);
st.db.with_read(|txn| {
let (_, perm, _) = txn.get_room_member(rid, &user)?;
api_ensure!(
perm.contains(MemberPermission::LIST_MEMBERS),
ApiError::PermissionDenied("the user does not have permission to get room members"),
);
let page_len = pagination.effective_page_len(&st);
let mut last_uid = None;
let members = txn
.list_room_members(
rid,
pagination.skip_token.unwrap_or(Id::MIN),
Some(page_len),
)?
.into_iter()
.map(|(uid, id_key, permission, last_seen_cid)| {
last_uid = Some(Id(uid));
RoomMember {
id_key,
permission,
last_seen_cid: (last_seen_cid != Id(0)).then_some(last_seen_cid),
}
})
.collect::<Vec<_>>();
let skip_token = (members.len() as u32 == page_len.get())
.then(|| last_uid.expect("page must not be empty"));
Ok(Json(RoomMemberList {
members,
skip_token,
}))
})
}