mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
Impl global room listing at /room
This commit is contained in:
parent
a9d5eb9631
commit
2b6fbe8794
4 changed files with 180 additions and 8 deletions
|
@ -16,6 +16,46 @@ paths:
|
|||
interested in (eg. chat from joined rooms).
|
||||
The message has type `Outgoing` in `blahd/src/ws.rs`.
|
||||
|
||||
/room:
|
||||
get:
|
||||
summary: List rooms on the server
|
||||
parameters:
|
||||
filter:
|
||||
in: query
|
||||
required: true
|
||||
description: |
|
||||
Either "public" or "joined".
|
||||
For "public", it returns all public rooms on the server.
|
||||
For "joined", `Authorization` must be provided and it will return
|
||||
rooms user have joined.
|
||||
page_len:
|
||||
in: query
|
||||
description:
|
||||
The maximum number of items returned in each page. This is only an
|
||||
advice and server can clamp it to a smaller value.
|
||||
page_token:
|
||||
in: query
|
||||
description:
|
||||
The page token returned from a previous list response to fetch the
|
||||
next page. NB. Other parameters (eg. `joined` and `page_len`)
|
||||
should be included (as the same value) for each page fetch.
|
||||
headers:
|
||||
Authorization:
|
||||
description: Proof of membership for private rooms. Required if `joined` is true.
|
||||
required: false
|
||||
schema:
|
||||
$ret: WithSig<AuthPayload>
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
application/json:
|
||||
$ref: '#/components/schema/ListRoom'
|
||||
401:
|
||||
description: Missing or invalid Authorization header.
|
||||
content:
|
||||
application/json:
|
||||
$ref: '#/components/schemas/ApiError'
|
||||
|
||||
/room/create:
|
||||
post:
|
||||
summary: Create a new room
|
||||
|
@ -196,9 +236,24 @@ components:
|
|||
message:
|
||||
type: string
|
||||
|
||||
RoomList:
|
||||
type: object
|
||||
required:
|
||||
- rooms
|
||||
properties:
|
||||
rooms:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/RoomMetadata'
|
||||
next_token:
|
||||
type: string
|
||||
description: An opaque token to fetch the next page.
|
||||
|
||||
RoomMetadata:
|
||||
type: object
|
||||
properties:
|
||||
ruuid:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
attrs:
|
||||
|
|
|
@ -21,6 +21,8 @@ CREATE TABLE IF NOT EXISTS `room_member` (
|
|||
PRIMARY KEY (`rid`, `uid`)
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS `member_room` ON `room_member` (`uid` ASC, `rid` ASC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `room_item` (
|
||||
`cid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`rid` INTEGER NOT NULL REFERENCES `room` ON DELETE CASCADE,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::num::NonZeroUsize;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -28,8 +29,8 @@ pub struct ServerConfig {
|
|||
pub listen: String,
|
||||
pub base_url: Url,
|
||||
|
||||
#[serde_inline_default(1024)]
|
||||
pub max_page_len: usize,
|
||||
#[serde_inline_default(1024.try_into().unwrap())]
|
||||
pub max_page_len: NonZeroUsize,
|
||||
#[serde_inline_default(4096)] // 4KiB
|
||||
pub max_request_len: usize,
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ use blah::types::{
|
|||
use config::Config;
|
||||
use ed25519_dalek::SIGNATURE_LENGTH;
|
||||
use middleware::{ApiError, OptionalAuth, SignedJson};
|
||||
use rusqlite::{named_params, params, Connection, OptionalExtension, Row};
|
||||
use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use utils::ExpiringSet;
|
||||
|
@ -149,6 +149,7 @@ async fn main_async(st: AppState) -> Result<()> {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/ws", get(handle_ws))
|
||||
.route("/room", get(room_list))
|
||||
.route("/room/create", post(room_create))
|
||||
.route("/room/:ruuid", get(room_get_metadata))
|
||||
// NB. Sync with `feed_url` and `next_url` generation.
|
||||
|
@ -196,6 +197,108 @@ async fn handle_ws(State(st): ArcState, ws: WebSocketUpgrade) -> Response {
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RoomList {
|
||||
rooms: Vec<RoomMetadata>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
skip_token: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct ListRoomParams {
|
||||
filter: ListRoomFilter,
|
||||
// Workaround: serde(flatten) breaks deserialization
|
||||
// See: https://github.com/nox/serde_urlencoded/issues/33
|
||||
skip_token: Option<u64>,
|
||||
top: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ListRoomFilter {
|
||||
Public,
|
||||
Joined,
|
||||
}
|
||||
|
||||
async fn room_list(
|
||||
st: ArcState,
|
||||
WithRejection(params, _): WithRejection<Query<ListRoomParams>, ApiError>,
|
||||
OptionalAuth(user): OptionalAuth,
|
||||
) -> Result<Json<RoomList>, ApiError> {
|
||||
let pagination = Pagination {
|
||||
skip_token: params.skip_token,
|
||||
top: params.top,
|
||||
};
|
||||
let page_len = pagination.effective_page_len(&st);
|
||||
let start_rid = pagination.skip_token.unwrap_or(0);
|
||||
|
||||
let query = |sql: &str, params: &[(&str, &dyn ToSql)]| -> Result<RoomList, ApiError> {
|
||||
let mut last_rid = None;
|
||||
let rooms = st
|
||||
.conn
|
||||
.lock()
|
||||
.unwrap()
|
||||
.prepare(sql)?
|
||||
.query_map(params, |row| {
|
||||
last_rid = Some(row.get::<_, u64>("rid")?);
|
||||
Ok(RoomMetadata {
|
||||
ruuid: row.get("ruuid")?,
|
||||
title: row.get("title")?,
|
||||
attrs: row.get("attrs")?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let skip_token = (rooms.len() == page_len).then_some(()).and(last_rid);
|
||||
Ok(RoomList { rooms, skip_token })
|
||||
};
|
||||
|
||||
match params.filter {
|
||||
ListRoomFilter::Public => query(
|
||||
r"
|
||||
SELECT `rid`, `ruuid`, `title`, `attrs`
|
||||
FROM `room`
|
||||
WHERE `rid` > :start_rid AND
|
||||
(`attrs` & :perm) = :perm
|
||||
ORDER BY `rid` ASC
|
||||
LIMIT :page_len
|
||||
",
|
||||
named_params! {
|
||||
":start_rid": start_rid,
|
||||
":page_len": page_len,
|
||||
":perm": RoomAttrs::PUBLIC_READABLE,
|
||||
},
|
||||
),
|
||||
ListRoomFilter::Joined => {
|
||||
let Some(user) = user else {
|
||||
return Err(error_response!(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"unauthorized",
|
||||
"missing Authorization header for listing joined rooms",
|
||||
));
|
||||
};
|
||||
query(
|
||||
r"
|
||||
SELECT `rid`, `ruuid`, `title`, `attrs`
|
||||
FROM `user`
|
||||
JOIN `room_member` USING (`uid`)
|
||||
JOIN `room` USING (`rid`)
|
||||
WHERE `userkey` = :userkey AND
|
||||
`rid` > :start_rid
|
||||
ORDER BY `rid` ASC
|
||||
LIMIT :page_len
|
||||
",
|
||||
named_params! {
|
||||
":start_rid": start_rid,
|
||||
":page_len": page_len,
|
||||
":userkey": user,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
async fn room_create(
|
||||
st: ArcState,
|
||||
SignedJson(params): SignedJson<CreateRoomPayload>,
|
||||
|
@ -293,6 +396,15 @@ struct Pagination {
|
|||
top: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
fn effective_page_len(&self, st: &AppState) -> usize {
|
||||
self.top
|
||||
.unwrap_or(usize::MAX.try_into().unwrap())
|
||||
.min(st.config.server.max_page_len)
|
||||
.get()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct RoomItems {
|
||||
items: Vec<ChatItem>,
|
||||
|
@ -331,7 +443,11 @@ async fn room_get_metadata(
|
|||
))
|
||||
})?;
|
||||
|
||||
Ok(Json(RoomMetadata { title, attrs }))
|
||||
Ok(Json(RoomMetadata {
|
||||
ruuid,
|
||||
title,
|
||||
attrs,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn room_get_feed(
|
||||
|
@ -436,6 +552,7 @@ struct FeedItemExtra {
|
|||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RoomMetadata {
|
||||
pub ruuid: Uuid,
|
||||
pub title: String,
|
||||
pub attrs: RoomAttrs,
|
||||
}
|
||||
|
@ -479,10 +596,7 @@ fn query_room_items(
|
|||
ruuid: Uuid,
|
||||
pagination: Pagination,
|
||||
) -> Result<(Vec<ChatItemWithId>, Option<u64>), ApiError> {
|
||||
let page_len = pagination
|
||||
.top
|
||||
.map_or(usize::MAX, |n| n.get())
|
||||
.min(st.config.server.max_page_len);
|
||||
let page_len = pagination.effective_page_len(st);
|
||||
let mut stmt = conn.prepare(
|
||||
r"
|
||||
SELECT `cid`, `timestamp`, `nonce`, `sig`, `userkey`, `sig`, `rich_text`
|
||||
|
|
Loading…
Add table
Reference in a new issue